Full parallel implementation of typesense and opensearch
This commit is contained in:
@@ -49,7 +49,7 @@ export default function StoryReadingPage() {
|
||||
));
|
||||
|
||||
// Convert to character position in the plain text content
|
||||
const textLength = story.contentPlain?.length || story.contentHtml.length;
|
||||
const textLength = story.contentPlain?.length || story.contentHtml?.length || 0;
|
||||
return Math.floor(scrollRatio * textLength);
|
||||
}, [story]);
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function StoryReadingPage() {
|
||||
const calculateReadingPercentage = useCallback((currentPosition: number): number => {
|
||||
if (!story) return 0;
|
||||
|
||||
const totalLength = story.contentPlain?.length || story.contentHtml.length;
|
||||
const totalLength = story.contentPlain?.length || story.contentHtml?.length || 0;
|
||||
if (totalLength === 0) return 0;
|
||||
|
||||
return Math.round((currentPosition / totalLength) * 100);
|
||||
@@ -67,7 +67,7 @@ export default function StoryReadingPage() {
|
||||
const scrollToCharacterPosition = useCallback((position: number) => {
|
||||
if (!contentRef.current || !story || hasScrolledToPosition) return;
|
||||
|
||||
const textLength = story.contentPlain?.length || story.contentHtml.length;
|
||||
const textLength = story.contentPlain?.length || story.contentHtml?.length || 0;
|
||||
if (textLength === 0 || position === 0) return;
|
||||
|
||||
const ratio = position / textLength;
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function CollectionReadingView({
|
||||
));
|
||||
|
||||
// Convert to character position in the plain text content
|
||||
const textLength = story.contentPlain?.length || story.contentHtml.length;
|
||||
const textLength = story.contentPlain?.length || story.contentHtml?.length || 0;
|
||||
return Math.floor(scrollRatio * textLength);
|
||||
}, [story]);
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function CollectionReadingView({
|
||||
const calculateReadingPercentage = useCallback((currentPosition: number): number => {
|
||||
if (!story) return 0;
|
||||
|
||||
const totalLength = story.contentPlain?.length || story.contentHtml.length;
|
||||
const totalLength = story.contentPlain?.length || story.contentHtml?.length || 0;
|
||||
if (totalLength === 0) return 0;
|
||||
|
||||
return Math.round((currentPosition / totalLength) * 100);
|
||||
@@ -58,7 +58,7 @@ export default function CollectionReadingView({
|
||||
const scrollToCharacterPosition = useCallback((position: number) => {
|
||||
if (!contentRef.current || !story || hasScrolledToPosition) return;
|
||||
|
||||
const textLength = story.contentPlain?.length || story.contentHtml.length;
|
||||
const textLength = story.contentPlain?.length || story.contentHtml?.length || 0;
|
||||
if (textLength === 0 || position === 0) return;
|
||||
|
||||
const ratio = position / textLength;
|
||||
|
||||
@@ -127,29 +127,6 @@ const FILTER_PRESETS: FilterPreset[] = [
|
||||
description: 'Stories that are part of a series',
|
||||
filters: { seriesFilter: 'series' },
|
||||
category: 'content'
|
||||
},
|
||||
|
||||
// Organization presets
|
||||
{
|
||||
id: 'well-tagged',
|
||||
label: '3+ tags',
|
||||
description: 'Well-tagged stories with 3 or more tags',
|
||||
filters: { minTagCount: 3 },
|
||||
category: 'organization'
|
||||
},
|
||||
{
|
||||
id: 'popular',
|
||||
label: 'Popular',
|
||||
description: 'Stories with above-average ratings',
|
||||
filters: { popularOnly: true },
|
||||
category: 'organization'
|
||||
},
|
||||
{
|
||||
id: 'hidden-gems',
|
||||
label: 'Hidden Gems',
|
||||
description: 'Underrated or unrated stories to discover',
|
||||
filters: { hiddenGemsOnly: true },
|
||||
category: 'organization'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -1,14 +1,39 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Button from '../ui/Button';
|
||||
import { storyApi, authorApi, databaseApi, configApi } from '../../lib/api';
|
||||
import { storyApi, authorApi, databaseApi, configApi, searchAdminApi } from '../../lib/api';
|
||||
|
||||
interface SystemSettingsProps {
|
||||
// No props needed - this component manages its own state
|
||||
}
|
||||
|
||||
export default function SystemSettings({}: SystemSettingsProps) {
|
||||
const [searchEngineStatus, setSearchEngineStatus] = useState<{
|
||||
currentEngine: string;
|
||||
dualWrite: boolean;
|
||||
typesenseAvailable: boolean;
|
||||
openSearchAvailable: boolean;
|
||||
loading: boolean;
|
||||
message: string;
|
||||
success?: boolean;
|
||||
}>({
|
||||
currentEngine: 'typesense',
|
||||
dualWrite: false,
|
||||
typesenseAvailable: false,
|
||||
openSearchAvailable: false,
|
||||
loading: false,
|
||||
message: ''
|
||||
});
|
||||
|
||||
const [openSearchStatus, setOpenSearchStatus] = useState<{
|
||||
reindex: { loading: boolean; message: string; success?: boolean };
|
||||
recreate: { loading: boolean; message: string; success?: boolean };
|
||||
}>({
|
||||
reindex: { loading: false, message: '' },
|
||||
recreate: { loading: false, message: '' }
|
||||
});
|
||||
|
||||
const [typesenseStatus, setTypesenseStatus] = useState<{
|
||||
reindex: { loading: boolean; message: string; success?: boolean };
|
||||
recreate: { loading: boolean; message: string; success?: boolean };
|
||||
@@ -419,13 +444,323 @@ export default function SystemSettings({}: SystemSettingsProps) {
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
// Search Engine Management Functions
|
||||
const loadSearchEngineStatus = async () => {
|
||||
try {
|
||||
const status = await searchAdminApi.getStatus();
|
||||
setSearchEngineStatus(prev => ({
|
||||
...prev,
|
||||
currentEngine: status.primaryEngine,
|
||||
dualWrite: status.dualWrite,
|
||||
typesenseAvailable: status.typesenseAvailable,
|
||||
openSearchAvailable: status.openSearchAvailable,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load search engine status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSwitchEngine = async (engine: string) => {
|
||||
setSearchEngineStatus(prev => ({ ...prev, loading: true, message: `Switching to ${engine}...` }));
|
||||
|
||||
try {
|
||||
const result = engine === 'opensearch'
|
||||
? await searchAdminApi.switchToOpenSearch()
|
||||
: await searchAdminApi.switchToTypesense();
|
||||
|
||||
setSearchEngineStatus(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
message: result.message,
|
||||
success: true,
|
||||
currentEngine: engine
|
||||
}));
|
||||
|
||||
setTimeout(() => {
|
||||
setSearchEngineStatus(prev => ({ ...prev, message: '', success: undefined }));
|
||||
}, 5000);
|
||||
} catch (error: any) {
|
||||
setSearchEngineStatus(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
message: error.message || 'Failed to switch engine',
|
||||
success: false
|
||||
}));
|
||||
|
||||
setTimeout(() => {
|
||||
setSearchEngineStatus(prev => ({ ...prev, message: '', success: undefined }));
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleDualWrite = async () => {
|
||||
const newDualWrite = !searchEngineStatus.dualWrite;
|
||||
setSearchEngineStatus(prev => ({
|
||||
...prev,
|
||||
loading: true,
|
||||
message: `${newDualWrite ? 'Enabling' : 'Disabling'} dual-write...`
|
||||
}));
|
||||
|
||||
try {
|
||||
const result = newDualWrite
|
||||
? await searchAdminApi.enableDualWrite()
|
||||
: await searchAdminApi.disableDualWrite();
|
||||
|
||||
setSearchEngineStatus(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
message: result.message,
|
||||
success: true,
|
||||
dualWrite: newDualWrite
|
||||
}));
|
||||
|
||||
setTimeout(() => {
|
||||
setSearchEngineStatus(prev => ({ ...prev, message: '', success: undefined }));
|
||||
}, 5000);
|
||||
} catch (error: any) {
|
||||
setSearchEngineStatus(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
message: error.message || 'Failed to toggle dual-write',
|
||||
success: false
|
||||
}));
|
||||
|
||||
setTimeout(() => {
|
||||
setSearchEngineStatus(prev => ({ ...prev, message: '', success: undefined }));
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenSearchReindex = async () => {
|
||||
setOpenSearchStatus(prev => ({
|
||||
...prev,
|
||||
reindex: { loading: true, message: 'Reindexing OpenSearch...', success: undefined }
|
||||
}));
|
||||
|
||||
try {
|
||||
const result = await searchAdminApi.reindexOpenSearch();
|
||||
|
||||
setOpenSearchStatus(prev => ({
|
||||
...prev,
|
||||
reindex: {
|
||||
loading: false,
|
||||
message: result.success ? result.message : (result.error || 'Reindex failed'),
|
||||
success: result.success
|
||||
}
|
||||
}));
|
||||
|
||||
setTimeout(() => {
|
||||
setOpenSearchStatus(prev => ({
|
||||
...prev,
|
||||
reindex: { loading: false, message: '', success: undefined }
|
||||
}));
|
||||
}, 8000);
|
||||
} catch (error: any) {
|
||||
setOpenSearchStatus(prev => ({
|
||||
...prev,
|
||||
reindex: {
|
||||
loading: false,
|
||||
message: error.message || 'Network error occurred',
|
||||
success: false
|
||||
}
|
||||
}));
|
||||
|
||||
setTimeout(() => {
|
||||
setOpenSearchStatus(prev => ({
|
||||
...prev,
|
||||
reindex: { loading: false, message: '', success: undefined }
|
||||
}));
|
||||
}, 8000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenSearchRecreate = async () => {
|
||||
setOpenSearchStatus(prev => ({
|
||||
...prev,
|
||||
recreate: { loading: true, message: 'Recreating OpenSearch indices...', success: undefined }
|
||||
}));
|
||||
|
||||
try {
|
||||
const result = await searchAdminApi.recreateOpenSearchIndices();
|
||||
|
||||
setOpenSearchStatus(prev => ({
|
||||
...prev,
|
||||
recreate: {
|
||||
loading: false,
|
||||
message: result.success ? result.message : (result.error || 'Recreation failed'),
|
||||
success: result.success
|
||||
}
|
||||
}));
|
||||
|
||||
setTimeout(() => {
|
||||
setOpenSearchStatus(prev => ({
|
||||
...prev,
|
||||
recreate: { loading: false, message: '', success: undefined }
|
||||
}));
|
||||
}, 8000);
|
||||
} catch (error: any) {
|
||||
setOpenSearchStatus(prev => ({
|
||||
...prev,
|
||||
recreate: {
|
||||
loading: false,
|
||||
message: error.message || 'Network error occurred',
|
||||
success: false
|
||||
}
|
||||
}));
|
||||
|
||||
setTimeout(() => {
|
||||
setOpenSearchStatus(prev => ({
|
||||
...prev,
|
||||
recreate: { loading: false, message: '', success: undefined }
|
||||
}));
|
||||
}, 8000);
|
||||
}
|
||||
};
|
||||
|
||||
// Load status on component mount
|
||||
useEffect(() => {
|
||||
loadSearchEngineStatus();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Typesense Search Management */}
|
||||
{/* Search Engine Management */}
|
||||
<div className="theme-card theme-shadow rounded-lg p-6">
|
||||
<h2 className="text-xl font-semibold theme-header mb-4">Search Index Management</h2>
|
||||
<h2 className="text-xl font-semibold theme-header mb-4">Search Engine Migration</h2>
|
||||
<p className="theme-text mb-6">
|
||||
Manage all Typesense search indexes (stories, authors, collections, etc.). Use these tools if search functionality isn't working properly.
|
||||
Manage the transition from Typesense to OpenSearch. Switch between engines, enable dual-write mode, and perform maintenance operations.
|
||||
</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Current Status */}
|
||||
<div className="border theme-border rounded-lg p-4">
|
||||
<h3 className="text-lg font-semibold theme-header mb-3">Current Configuration</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span>Primary Engine:</span>
|
||||
<span className={`font-medium ${searchEngineStatus.currentEngine === 'opensearch' ? 'text-blue-600 dark:text-blue-400' : 'text-green-600 dark:text-green-400'}`}>
|
||||
{searchEngineStatus.currentEngine.charAt(0).toUpperCase() + searchEngineStatus.currentEngine.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Dual-Write:</span>
|
||||
<span className={`font-medium ${searchEngineStatus.dualWrite ? 'text-orange-600 dark:text-orange-400' : 'text-gray-600 dark:text-gray-400'}`}>
|
||||
{searchEngineStatus.dualWrite ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Typesense:</span>
|
||||
<span className={`font-medium ${searchEngineStatus.typesenseAvailable ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
{searchEngineStatus.typesenseAvailable ? 'Available' : 'Unavailable'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>OpenSearch:</span>
|
||||
<span className={`font-medium ${searchEngineStatus.openSearchAvailable ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
{searchEngineStatus.openSearchAvailable ? 'Available' : 'Unavailable'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Engine Switching */}
|
||||
<div className="border theme-border rounded-lg p-4">
|
||||
<h3 className="text-lg font-semibold theme-header mb-3">Engine Controls</h3>
|
||||
<div className="flex flex-col sm:flex-row gap-3 mb-4">
|
||||
<Button
|
||||
onClick={() => handleSwitchEngine('typesense')}
|
||||
disabled={searchEngineStatus.loading || !searchEngineStatus.typesenseAvailable || searchEngineStatus.currentEngine === 'typesense'}
|
||||
variant={searchEngineStatus.currentEngine === 'typesense' ? 'primary' : 'ghost'}
|
||||
className="flex-1"
|
||||
>
|
||||
{searchEngineStatus.currentEngine === 'typesense' ? '✓ Typesense (Active)' : 'Switch to Typesense'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleSwitchEngine('opensearch')}
|
||||
disabled={searchEngineStatus.loading || !searchEngineStatus.openSearchAvailable || searchEngineStatus.currentEngine === 'opensearch'}
|
||||
variant={searchEngineStatus.currentEngine === 'opensearch' ? 'primary' : 'ghost'}
|
||||
className="flex-1"
|
||||
>
|
||||
{searchEngineStatus.currentEngine === 'opensearch' ? '✓ OpenSearch (Active)' : 'Switch to OpenSearch'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleToggleDualWrite}
|
||||
disabled={searchEngineStatus.loading}
|
||||
variant={searchEngineStatus.dualWrite ? 'secondary' : 'ghost'}
|
||||
className="flex-1"
|
||||
>
|
||||
{searchEngineStatus.dualWrite ? 'Disable Dual-Write' : 'Enable Dual-Write'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{searchEngineStatus.message && (
|
||||
<div className={`text-sm p-3 rounded mb-3 ${
|
||||
searchEngineStatus.success
|
||||
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
|
||||
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
|
||||
}`}>
|
||||
{searchEngineStatus.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* OpenSearch Operations */}
|
||||
<div className="border theme-border rounded-lg p-4">
|
||||
<h3 className="text-lg font-semibold theme-header mb-3">OpenSearch Operations</h3>
|
||||
<p className="text-sm theme-text mb-4">
|
||||
Perform maintenance operations on OpenSearch indices. Use these if OpenSearch isn't returning expected results.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3 mb-4">
|
||||
<Button
|
||||
onClick={handleOpenSearchReindex}
|
||||
disabled={openSearchStatus.reindex.loading || openSearchStatus.recreate.loading || !searchEngineStatus.openSearchAvailable}
|
||||
loading={openSearchStatus.reindex.loading}
|
||||
variant="ghost"
|
||||
className="flex-1"
|
||||
>
|
||||
{openSearchStatus.reindex.loading ? 'Reindexing...' : '🔄 Reindex OpenSearch'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleOpenSearchRecreate}
|
||||
disabled={openSearchStatus.reindex.loading || openSearchStatus.recreate.loading || !searchEngineStatus.openSearchAvailable}
|
||||
loading={openSearchStatus.recreate.loading}
|
||||
variant="secondary"
|
||||
className="flex-1"
|
||||
>
|
||||
{openSearchStatus.recreate.loading ? 'Recreating...' : '🏗️ Recreate Indices'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* OpenSearch Status Messages */}
|
||||
{openSearchStatus.reindex.message && (
|
||||
<div className={`text-sm p-3 rounded mb-3 ${
|
||||
openSearchStatus.reindex.success
|
||||
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
|
||||
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
|
||||
}`}>
|
||||
{openSearchStatus.reindex.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{openSearchStatus.recreate.message && (
|
||||
<div className={`text-sm p-3 rounded mb-3 ${
|
||||
openSearchStatus.recreate.success
|
||||
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
|
||||
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
|
||||
}`}>
|
||||
{openSearchStatus.recreate.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legacy Typesense Management */}
|
||||
<div className="theme-card theme-shadow rounded-lg p-6">
|
||||
<h2 className="text-xl font-semibold theme-header mb-4">Legacy Typesense Management</h2>
|
||||
<p className="theme-text mb-6">
|
||||
Manage Typesense search indexes (for backwards compatibility and during migration). These tools will be removed once migration is complete.
|
||||
</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -17,17 +17,34 @@ interface StoryCardProps {
|
||||
onSelect?: () => void;
|
||||
}
|
||||
|
||||
export default function StoryCard({
|
||||
story,
|
||||
viewMode,
|
||||
onUpdate,
|
||||
showSelection = false,
|
||||
isSelected = false,
|
||||
onSelect
|
||||
export default function StoryCard({
|
||||
story,
|
||||
viewMode,
|
||||
onUpdate,
|
||||
showSelection = false,
|
||||
isSelected = false,
|
||||
onSelect
|
||||
}: StoryCardProps) {
|
||||
const [rating, setRating] = useState(story.rating || 0);
|
||||
const [updating, setUpdating] = useState(false);
|
||||
|
||||
// Helper function to get tags from either tags array or tagNames array
|
||||
const getTags = () => {
|
||||
if (Array.isArray(story.tags) && story.tags.length > 0) {
|
||||
return story.tags;
|
||||
}
|
||||
if (Array.isArray(story.tagNames) && story.tagNames.length > 0) {
|
||||
// Convert tagNames to Tag objects for display compatibility
|
||||
return story.tagNames.map((name, index) => ({
|
||||
id: `tag-${index}`, // Temporary ID for display
|
||||
name: name
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const displayTags = getTags();
|
||||
|
||||
const handleRatingClick = async (e: React.MouseEvent, newRating: number) => {
|
||||
// Prevent default and stop propagation to avoid triggering navigation
|
||||
e.preventDefault();
|
||||
@@ -58,7 +75,7 @@ export default function StoryCard({
|
||||
const calculateReadingPercentage = (story: Story): number => {
|
||||
if (!story.readingPosition) return 0;
|
||||
|
||||
const totalLength = story.contentPlain?.length || story.contentHtml.length;
|
||||
const totalLength = story.contentPlain?.length || story.contentHtml?.length || 0;
|
||||
if (totalLength === 0) return 0;
|
||||
|
||||
return Math.round((story.readingPosition / totalLength) * 100);
|
||||
@@ -124,9 +141,9 @@ export default function StoryCard({
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
{Array.isArray(story.tags) && story.tags.length > 0 && (
|
||||
{displayTags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{story.tags.slice(0, 3).map((tag) => (
|
||||
{displayTags.slice(0, 3).map((tag) => (
|
||||
<TagDisplay
|
||||
key={tag.id}
|
||||
tag={tag}
|
||||
@@ -134,9 +151,9 @@ export default function StoryCard({
|
||||
clickable={false}
|
||||
/>
|
||||
))}
|
||||
{story.tags.length > 3 && (
|
||||
{displayTags.length > 3 && (
|
||||
<span className="px-2 py-1 text-xs theme-text">
|
||||
+{story.tags.length - 3} more
|
||||
+{displayTags.length - 3} more
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -260,9 +277,9 @@ export default function StoryCard({
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
{Array.isArray(story.tags) && story.tags.length > 0 && (
|
||||
{displayTags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{story.tags.slice(0, 2).map((tag) => (
|
||||
{displayTags.slice(0, 2).map((tag) => (
|
||||
<TagDisplay
|
||||
key={tag.id}
|
||||
tag={tag}
|
||||
@@ -270,9 +287,9 @@ export default function StoryCard({
|
||||
clickable={false}
|
||||
/>
|
||||
))}
|
||||
{story.tags.length > 2 && (
|
||||
{displayTags.length > 2 && (
|
||||
<span className="px-2 py-1 text-xs theme-text">
|
||||
+{story.tags.length - 2}
|
||||
+{displayTags.length - 2}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -611,6 +611,79 @@ export const configApi = {
|
||||
},
|
||||
};
|
||||
|
||||
// Search Engine Management API
|
||||
export const searchAdminApi = {
|
||||
// Get migration status
|
||||
getStatus: async (): Promise<{
|
||||
primaryEngine: string;
|
||||
dualWrite: boolean;
|
||||
typesenseAvailable: boolean;
|
||||
openSearchAvailable: boolean;
|
||||
}> => {
|
||||
const response = await api.get('/admin/search/status');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Configure search engine
|
||||
configure: async (config: { engine: string; dualWrite: boolean }): Promise<{ message: string }> => {
|
||||
const response = await api.post('/admin/search/configure', config);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Enable/disable dual-write
|
||||
enableDualWrite: async (): Promise<{ message: string }> => {
|
||||
const response = await api.post('/admin/search/dual-write/enable');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
disableDualWrite: async (): Promise<{ message: string }> => {
|
||||
const response = await api.post('/admin/search/dual-write/disable');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Switch engines
|
||||
switchToOpenSearch: async (): Promise<{ message: string }> => {
|
||||
const response = await api.post('/admin/search/switch/opensearch');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
switchToTypesense: async (): Promise<{ message: string }> => {
|
||||
const response = await api.post('/admin/search/switch/typesense');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Emergency rollback
|
||||
emergencyRollback: async (): Promise<{ message: string }> => {
|
||||
const response = await api.post('/admin/search/emergency-rollback');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// OpenSearch operations
|
||||
reindexOpenSearch: async (): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
storiesCount?: number;
|
||||
authorsCount?: number;
|
||||
totalCount?: number;
|
||||
error?: string;
|
||||
}> => {
|
||||
const response = await api.post('/admin/search/opensearch/reindex');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
recreateOpenSearchIndices: async (): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
storiesCount?: number;
|
||||
authorsCount?: number;
|
||||
totalCount?: number;
|
||||
error?: string;
|
||||
}> => {
|
||||
const response = await api.post('/admin/search/opensearch/recreate');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
// Collection endpoints
|
||||
export const collectionApi = {
|
||||
getCollections: async (params?: {
|
||||
|
||||
Reference in New Issue
Block a user