solr fix
This commit is contained in:
196
SOLR_LIBRARY_MIGRATION.md
Normal file
196
SOLR_LIBRARY_MIGRATION.md
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
# Solr Library Separation Migration Guide
|
||||||
|
|
||||||
|
This guide explains how to migrate existing StoryCove deployments to support proper library separation in Solr search.
|
||||||
|
|
||||||
|
## What Changed
|
||||||
|
|
||||||
|
The Solr service has been enhanced to support multi-tenant library separation by:
|
||||||
|
- Adding a `libraryId` field to all Solr documents
|
||||||
|
- Filtering all search queries by the current library context
|
||||||
|
- Ensuring complete data isolation between libraries
|
||||||
|
|
||||||
|
## Migration Options
|
||||||
|
|
||||||
|
### Option 1: Docker Volume Reset (Recommended for Docker)
|
||||||
|
|
||||||
|
**Best for**: Development, staging, and Docker-based deployments where data loss is acceptable.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop the application
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Remove only the Solr data volume (preserves database and images)
|
||||||
|
docker volume rm storycove_solr_data
|
||||||
|
|
||||||
|
# Restart - Solr will recreate cores with new schema
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Wait for services to start, then trigger reindex via admin panel
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pros**: Clean, simple, guaranteed to work
|
||||||
|
**Cons**: Requires downtime, loses existing search index
|
||||||
|
|
||||||
|
### Option 2: Schema API Migration (Production Safe)
|
||||||
|
|
||||||
|
**Best for**: Production environments where you need to preserve uptime.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add libraryId field to stories core
|
||||||
|
curl -X POST "http://your-solr-host:8983/solr/storycove_stories/schema" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"add-field": {
|
||||||
|
"name": "libraryId",
|
||||||
|
"type": "string",
|
||||||
|
"indexed": true,
|
||||||
|
"stored": true,
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Add libraryId field to authors core
|
||||||
|
curl -X POST "http://your-solr-host:8983/solr/storycove_authors/schema" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"add-field": {
|
||||||
|
"name": "libraryId",
|
||||||
|
"type": "string",
|
||||||
|
"indexed": true,
|
||||||
|
"stored": true,
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Then use the admin migration endpoint
|
||||||
|
curl -X POST "http://your-app-host/api/admin/search/solr/migrate-library-schema" \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pros**: No downtime, preserves service availability
|
||||||
|
**Cons**: More complex, requires API access
|
||||||
|
|
||||||
|
### Option 3: Application-Level Migration (Recommended for Production)
|
||||||
|
|
||||||
|
**Best for**: Production environments with proper admin access.
|
||||||
|
|
||||||
|
1. **Deploy the code changes** to your environment
|
||||||
|
2. **Access the admin panel** of your application
|
||||||
|
3. **Navigate to search settings**
|
||||||
|
4. **Use the "Migrate Library Schema" button** or API endpoint:
|
||||||
|
```
|
||||||
|
POST /api/admin/search/solr/migrate-library-schema
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pros**: User-friendly, handles all complexity internally
|
||||||
|
**Cons**: Requires admin access to application
|
||||||
|
|
||||||
|
## Step-by-Step Migration Process
|
||||||
|
|
||||||
|
### For Docker Deployments
|
||||||
|
|
||||||
|
1. **Backup your data** (optional but recommended):
|
||||||
|
```bash
|
||||||
|
# Backup database
|
||||||
|
docker-compose exec postgres pg_dump -U storycove storycove > backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Pull the latest code** with library separation fixes
|
||||||
|
|
||||||
|
3. **Choose migration approach**:
|
||||||
|
- **Quick & Clean**: Use Option 1 (volume reset)
|
||||||
|
- **Production**: Use Option 2 or 3
|
||||||
|
|
||||||
|
4. **Verify migration**:
|
||||||
|
- Log in with different library passwords
|
||||||
|
- Perform searches to confirm isolation
|
||||||
|
- Check that new content gets indexed with library IDs
|
||||||
|
|
||||||
|
### For Kubernetes/Production Deployments
|
||||||
|
|
||||||
|
1. **Update your deployment** with the new container images
|
||||||
|
|
||||||
|
2. **Add the libraryId field** to Solr schema using Option 2
|
||||||
|
|
||||||
|
3. **Use the migration endpoint** (Option 3):
|
||||||
|
```bash
|
||||||
|
kubectl exec -it deployment/storycove-backend -- \
|
||||||
|
curl -X POST http://localhost:8080/api/admin/search/solr/migrate-library-schema
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Monitor logs** for successful migration
|
||||||
|
|
||||||
|
## Verification Steps
|
||||||
|
|
||||||
|
After migration, verify that library separation is working:
|
||||||
|
|
||||||
|
1. **Test with multiple libraries**:
|
||||||
|
- Log in with Library A password
|
||||||
|
- Add/search content
|
||||||
|
- Log in with Library B password
|
||||||
|
- Confirm Library A content is not visible
|
||||||
|
|
||||||
|
2. **Check Solr directly** (if accessible):
|
||||||
|
```bash
|
||||||
|
# Should show documents with libraryId field
|
||||||
|
curl "http://solr:8983/solr/storycove_stories/select?q=*:*&fl=id,title,libraryId&rows=5"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Monitor application logs** for any library separation errors
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "unknown field 'libraryId'" Error
|
||||||
|
|
||||||
|
This means the Solr schema wasn't updated. Solutions:
|
||||||
|
- Use Option 1 (volume reset) for clean restart
|
||||||
|
- Use Option 2 (Schema API) to add the field manually
|
||||||
|
- Check that schema files contain the libraryId field definition
|
||||||
|
|
||||||
|
### Migration Endpoint Returns Error
|
||||||
|
|
||||||
|
Common causes:
|
||||||
|
- Solr is not available (check connectivity)
|
||||||
|
- No active library context (ensure user is authenticated)
|
||||||
|
- Insufficient permissions (check JWT token/authentication)
|
||||||
|
|
||||||
|
### Search Results Still Mixed
|
||||||
|
|
||||||
|
This indicates incomplete migration:
|
||||||
|
- Clear all Solr data and reindex completely
|
||||||
|
- Verify that all documents have libraryId field
|
||||||
|
- Check that search queries include library filters
|
||||||
|
|
||||||
|
## Environment-Specific Notes
|
||||||
|
|
||||||
|
### Development
|
||||||
|
- Use Option 1 (volume reset) for simplicity
|
||||||
|
- Data loss is acceptable in dev environments
|
||||||
|
|
||||||
|
### Staging
|
||||||
|
- Use Option 2 or 3 to test production migration procedures
|
||||||
|
- Verify migration process before applying to production
|
||||||
|
|
||||||
|
### Production
|
||||||
|
- **Always backup data first**
|
||||||
|
- Use Option 2 (Schema API) or Option 3 (Admin endpoint)
|
||||||
|
- Plan for brief performance impact during reindexing
|
||||||
|
- Monitor system resources during bulk reindexing
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
- **Reindexing time**: Depends on data size (typically 1000 docs/second)
|
||||||
|
- **Memory usage**: May increase during bulk indexing
|
||||||
|
- **Search performance**: Minimal impact from library filtering
|
||||||
|
- **Storage**: Slight increase due to libraryId field
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
|
||||||
|
If issues occur:
|
||||||
|
|
||||||
|
1. **Immediate**: Restart Solr to previous state (if using Option 1)
|
||||||
|
2. **Schema revert**: Remove libraryId field via Schema API
|
||||||
|
3. **Code rollback**: Deploy previous version without library separation
|
||||||
|
4. **Data restore**: Restore from backup if necessary
|
||||||
|
|
||||||
|
This migration enables proper multi-tenant isolation while maintaining search performance and functionality.
|
||||||
@@ -160,4 +160,71 @@ public class AdminSearchController {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate to library-aware Solr schema.
|
||||||
|
* This endpoint handles the migration from non-library-aware to library-aware indexing.
|
||||||
|
* It clears existing data and reindexes with library context.
|
||||||
|
*/
|
||||||
|
@PostMapping("/solr/migrate-library-schema")
|
||||||
|
public ResponseEntity<Map<String, Object>> migrateLibrarySchema() {
|
||||||
|
try {
|
||||||
|
logger.info("Starting Solr library schema migration");
|
||||||
|
|
||||||
|
if (!searchServiceAdapter.isSearchServiceAvailable()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
|
"success", false,
|
||||||
|
"error", "Solr is not available or healthy"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (solrService == null) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
|
"success", false,
|
||||||
|
"error", "Solr service not available"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Clearing existing Solr data for library schema migration");
|
||||||
|
|
||||||
|
// Note: This assumes the libraryId field has been added to the Solr schema
|
||||||
|
// Either manually or via schema restart with updated schema files
|
||||||
|
|
||||||
|
// Clear existing data that doesn't have libraryId
|
||||||
|
solrService.recreateIndices();
|
||||||
|
|
||||||
|
// Get all data and reindex with library context
|
||||||
|
List<Story> allStories = storyService.findAllWithAssociations();
|
||||||
|
List<Author> allAuthors = authorService.findAllWithStories();
|
||||||
|
|
||||||
|
logger.info("Reindexing {} stories and {} authors with library context",
|
||||||
|
allStories.size(), allAuthors.size());
|
||||||
|
|
||||||
|
// Bulk index everything (will now include libraryId from current library context)
|
||||||
|
solrService.bulkIndexStories(allStories);
|
||||||
|
solrService.bulkIndexAuthors(allAuthors);
|
||||||
|
|
||||||
|
int totalIndexed = allStories.size() + allAuthors.size();
|
||||||
|
|
||||||
|
logger.info("Solr library schema migration completed successfully");
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"success", true,
|
||||||
|
"message", String.format("Library schema migration completed. Reindexed %d stories and %d authors with library context.",
|
||||||
|
allStories.size(), allAuthors.size()),
|
||||||
|
"storiesCount", allStories.size(),
|
||||||
|
"authorsCount", allAuthors.size(),
|
||||||
|
"totalCount", totalIndexed,
|
||||||
|
"note", "Ensure libraryId field exists in Solr schema before running this migration"
|
||||||
|
));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Error during Solr library schema migration", e);
|
||||||
|
return ResponseEntity.internalServerError().body(Map.of(
|
||||||
|
"success", false,
|
||||||
|
"error", "Library schema migration failed: " + e.getMessage(),
|
||||||
|
"details", "Make sure the libraryId field has been added to both stories and authors Solr cores"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,6 @@ import org.apache.solr.client.solrj.response.UpdateResponse;
|
|||||||
import org.apache.solr.common.SolrDocument;
|
import org.apache.solr.common.SolrDocument;
|
||||||
import org.apache.solr.common.SolrDocumentList;
|
import org.apache.solr.common.SolrDocumentList;
|
||||||
import org.apache.solr.common.SolrInputDocument;
|
import org.apache.solr.common.SolrInputDocument;
|
||||||
import org.apache.solr.common.params.ModifiableSolrParams;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -50,6 +49,9 @@ public class SolrService {
|
|||||||
@Lazy
|
@Lazy
|
||||||
private ReadingTimeService readingTimeService;
|
private ReadingTimeService readingTimeService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private LibraryService libraryService;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void initializeCores() {
|
public void initializeCores() {
|
||||||
if (!isAvailable()) {
|
if (!isAvailable()) {
|
||||||
@@ -287,6 +289,12 @@ public class SolrService {
|
|||||||
doc.addField("updatedAt", formatDateTime(story.getUpdatedAt()));
|
doc.addField("updatedAt", formatDateTime(story.getUpdatedAt()));
|
||||||
doc.addField("dateAdded", formatDateTime(story.getCreatedAt()));
|
doc.addField("dateAdded", formatDateTime(story.getCreatedAt()));
|
||||||
|
|
||||||
|
// Add library ID for multi-tenant separation
|
||||||
|
String currentLibraryId = getCurrentLibraryId();
|
||||||
|
if (currentLibraryId != null) {
|
||||||
|
doc.addField("libraryId", currentLibraryId);
|
||||||
|
}
|
||||||
|
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,6 +328,12 @@ public class SolrService {
|
|||||||
doc.addField("createdAt", formatDateTime(author.getCreatedAt()));
|
doc.addField("createdAt", formatDateTime(author.getCreatedAt()));
|
||||||
doc.addField("updatedAt", formatDateTime(author.getUpdatedAt()));
|
doc.addField("updatedAt", formatDateTime(author.getUpdatedAt()));
|
||||||
|
|
||||||
|
// Add library ID for multi-tenant separation
|
||||||
|
String currentLibraryId = getCurrentLibraryId();
|
||||||
|
if (currentLibraryId != null) {
|
||||||
|
doc.addField("libraryId", currentLibraryId);
|
||||||
|
}
|
||||||
|
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,6 +350,20 @@ public class SolrService {
|
|||||||
return solrClient != null;
|
return solrClient != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current library ID for multi-tenant document separation.
|
||||||
|
* Falls back to "default" if no library is active.
|
||||||
|
*/
|
||||||
|
private String getCurrentLibraryId() {
|
||||||
|
try {
|
||||||
|
String libraryId = libraryService.getCurrentLibraryId();
|
||||||
|
return libraryId != null ? libraryId : "default";
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Could not get current library ID, using 'default': {}", e.getMessage());
|
||||||
|
return "default";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean testConnection() {
|
public boolean testConnection() {
|
||||||
if (!isAvailable()) {
|
if (!isAvailable()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -465,6 +493,10 @@ public class SolrService {
|
|||||||
|
|
||||||
solrQuery.setRows(limit);
|
solrQuery.setRows(limit);
|
||||||
|
|
||||||
|
// Add library filter for multi-tenant separation
|
||||||
|
String currentLibraryId = getCurrentLibraryId();
|
||||||
|
solrQuery.addFilterQuery("libraryId:\"" + escapeQueryChars(currentLibraryId) + "\"");
|
||||||
|
|
||||||
// Sort by storyCount if available, otherwise by name
|
// Sort by storyCount if available, otherwise by name
|
||||||
solrQuery.setSort("storyCount", SolrQuery.ORDER.desc);
|
solrQuery.setSort("storyCount", SolrQuery.ORDER.desc);
|
||||||
solrQuery.addSort("name", SolrQuery.ORDER.asc);
|
solrQuery.addSort("name", SolrQuery.ORDER.asc);
|
||||||
@@ -496,6 +528,10 @@ public class SolrService {
|
|||||||
solrQuery.setFacetMinCount(1);
|
solrQuery.setFacetMinCount(1);
|
||||||
solrQuery.setFacetLimit(limit);
|
solrQuery.setFacetLimit(limit);
|
||||||
|
|
||||||
|
// Add library filter for multi-tenant separation
|
||||||
|
String currentLibraryId = getCurrentLibraryId();
|
||||||
|
solrQuery.addFilterQuery("libraryId:\"" + escapeQueryChars(currentLibraryId) + "\"");
|
||||||
|
|
||||||
QueryResponse response = solrClient.query(properties.getCores().getStories(), solrQuery);
|
QueryResponse response = solrClient.query(properties.getCores().getStories(), solrQuery);
|
||||||
|
|
||||||
return response.getFacetField("tagNames_facet").getValues().stream()
|
return response.getFacetField("tagNames_facet").getValues().stream()
|
||||||
@@ -523,6 +559,10 @@ public class SolrService {
|
|||||||
solrQuery.setSort("random_" + System.currentTimeMillis(), SolrQuery.ORDER.asc);
|
solrQuery.setSort("random_" + System.currentTimeMillis(), SolrQuery.ORDER.asc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add library filter for multi-tenant separation
|
||||||
|
String currentLibraryId = getCurrentLibraryId();
|
||||||
|
solrQuery.addFilterQuery("libraryId:\"" + escapeQueryChars(currentLibraryId) + "\"");
|
||||||
|
|
||||||
QueryResponse response = solrClient.query(properties.getCores().getStories(), solrQuery);
|
QueryResponse response = solrClient.query(properties.getCores().getStories(), solrQuery);
|
||||||
|
|
||||||
if (response.getResults().size() > 0) {
|
if (response.getResults().size() > 0) {
|
||||||
@@ -750,6 +790,10 @@ public class SolrService {
|
|||||||
|
|
||||||
List<String> filters = new ArrayList<>();
|
List<String> filters = new ArrayList<>();
|
||||||
|
|
||||||
|
// Library filter - ensure multi-tenant data separation
|
||||||
|
String currentLibraryId = getCurrentLibraryId();
|
||||||
|
filters.add("libraryId:\"" + escapeQueryChars(currentLibraryId) + "\"");
|
||||||
|
|
||||||
// Tag filters - use facet field for exact matching
|
// Tag filters - use facet field for exact matching
|
||||||
if (tags != null && !tags.isEmpty()) {
|
if (tags != null && !tags.isEmpty()) {
|
||||||
String tagFilter = tags.stream()
|
String tagFilter = tags.stream()
|
||||||
|
|||||||
@@ -448,9 +448,15 @@ function EditorContent({
|
|||||||
);
|
);
|
||||||
const [isScrollable, setIsScrollable] = useState(true); // Default to scrollable
|
const [isScrollable, setIsScrollable] = useState(true); // Default to scrollable
|
||||||
|
|
||||||
// Sync HTML value with prop changes
|
// Sync HTML value with prop changes (but not for internal changes)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
debug.log('🔄 Editor value changed:', { valueLength: value?.length, valuePreview: value?.substring(0, 100) });
|
// Skip re-initialization if this change came from the editor itself
|
||||||
|
if (isInternalChange.current) {
|
||||||
|
debug.log('🔄 Skipping re-initialization for internal change');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug.log('🔄 Editor value changed externally:', { valueLength: value?.length, valuePreview: value?.substring(0, 100) });
|
||||||
setPortableTextValue(htmlToPortableTextBlocks(value));
|
setPortableTextValue(htmlToPortableTextBlocks(value));
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
@@ -459,13 +465,23 @@ function EditorContent({
|
|||||||
debug.log('📝 Portable text blocks updated:', { blockCount: portableTextValue.length, blocks: portableTextValue });
|
debug.log('📝 Portable text blocks updated:', { blockCount: portableTextValue.length, blocks: portableTextValue });
|
||||||
}, [portableTextValue]);
|
}, [portableTextValue]);
|
||||||
|
|
||||||
|
// Track if changes are coming from internal editor changes
|
||||||
|
const isInternalChange = useRef(false);
|
||||||
|
|
||||||
// Handle content changes using the EventListenerPlugin
|
// Handle content changes using the EventListenerPlugin
|
||||||
const handleEditorChange = useCallback((event: any) => {
|
const handleEditorChange = useCallback((event: any) => {
|
||||||
if (event.type === 'mutation') {
|
if (event.type === 'mutation') {
|
||||||
debug.log('📝 Editor content changed via EventListener:', { valueLength: event.value?.length });
|
debug.log('📝 Editor content changed via EventListener:', { valueLength: event.value?.length });
|
||||||
const html = portableTextToHtml(event.value);
|
const html = portableTextToHtml(event.value);
|
||||||
debug.log('📝 Converted to HTML:', { htmlLength: html.length, htmlPreview: html.substring(0, 200) });
|
debug.log('📝 Converted to HTML:', { htmlLength: html.length, htmlPreview: html.substring(0, 200) });
|
||||||
|
|
||||||
|
// Mark this as an internal change to prevent re-initialization
|
||||||
|
isInternalChange.current = true;
|
||||||
onChange(html);
|
onChange(html);
|
||||||
|
// Reset flag after a short delay to allow external changes
|
||||||
|
setTimeout(() => {
|
||||||
|
isInternalChange.current = false;
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
}, [onChange]);
|
}, [onChange]);
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,9 @@
|
|||||||
<field name="urls" type="strings" indexed="true" stored="true"/>
|
<field name="urls" type="strings" indexed="true" stored="true"/>
|
||||||
<field name="avatarImagePath" type="string" indexed="false" stored="true"/>
|
<field name="avatarImagePath" type="string" indexed="false" stored="true"/>
|
||||||
|
|
||||||
|
<!-- Multi-tenant Library Separation -->
|
||||||
|
<field name="libraryId" type="string" indexed="true" stored="true" required="true"/>
|
||||||
|
|
||||||
<!-- Timestamp Fields -->
|
<!-- Timestamp Fields -->
|
||||||
<field name="createdAt" type="pdate" indexed="true" stored="true"/>
|
<field name="createdAt" type="pdate" indexed="true" stored="true"/>
|
||||||
<field name="updatedAt" type="pdate" indexed="true" stored="true"/>
|
<field name="updatedAt" type="pdate" indexed="true" stored="true"/>
|
||||||
|
|||||||
@@ -77,6 +77,9 @@
|
|||||||
<field name="averageStoryRating" type="pdouble" indexed="true" stored="true"/>
|
<field name="averageStoryRating" type="pdouble" indexed="true" stored="true"/>
|
||||||
<field name="volume" type="pint" indexed="true" stored="true"/>
|
<field name="volume" type="pint" indexed="true" stored="true"/>
|
||||||
|
|
||||||
|
<!-- Multi-tenant Library Separation -->
|
||||||
|
<field name="libraryId" type="string" indexed="true" stored="true" required="true"/>
|
||||||
|
|
||||||
<!-- Reading Status Fields -->
|
<!-- Reading Status Fields -->
|
||||||
<field name="isRead" type="boolean" indexed="true" stored="true"/>
|
<field name="isRead" type="boolean" indexed="true" stored="true"/>
|
||||||
<field name="readingPosition" type="pint" indexed="true" stored="true"/>
|
<field name="readingPosition" type="pint" indexed="true" stored="true"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user