From 030ac94f4eceac2464710443e346850f418259ed Mon Sep 17 00:00:00 2001 From: Guo Ziang Date: Mon, 23 Jun 2025 15:50:29 +0800 Subject: [PATCH 1/6] feat: add rebuild indexes API and UI for documents - Implemented a new API endpoint to rebuild specified types of indexes for a document. - Added corresponding request and response models for the rebuild indexes functionality. - Updated the frontend to include a modal for selecting index types to rebuild. - Enhanced the document management UI with a rebuild index option in the dropdown menu. - Added localization support for rebuild index messages in both English and Chinese. --- frontend/src/api/apis/default-api.ts | 127 ++++++++++++++++++ frontend/src/api/models/index.ts | 2 + .../src/api/models/rebuild-indexes-request.ts | 39 ++++++ frontend/src/api/models/success-response.ts | 36 +++++ frontend/src/api/openapi.merged.yaml | 67 +++++++++ frontend/src/locales/en-US.ts | 7 + frontend/src/locales/zh-CN.ts | 7 + .../collections/$collectionId/documents.tsx | 113 +++++++++++++++- 8 files changed, 395 insertions(+), 3 deletions(-) create mode 100644 frontend/src/api/models/rebuild-indexes-request.ts create mode 100644 frontend/src/api/models/success-response.ts diff --git a/frontend/src/api/apis/default-api.ts b/frontend/src/api/apis/default-api.ts index 922845c9a..2aaab76c3 100644 --- a/frontend/src/api/apis/default-api.ts +++ b/frontend/src/api/apis/default-api.ts @@ -102,6 +102,8 @@ import type { ModelConfigList } from '../models'; // @ts-ignore import type { PromptTemplateList } from '../models'; // @ts-ignore +import type { RebuildIndexesRequest } from '../models'; +// @ts-ignore import type { Register } from '../models'; // @ts-ignore import type { SearchRequest } from '../models'; @@ -110,6 +112,8 @@ import type { SearchResult } from '../models'; // @ts-ignore import type { SearchResultList } from '../models'; // @ts-ignore +import type { SuccessResponse } from '../models'; +// @ts-ignore import type { TagFilterRequest } from '../models'; // @ts-ignore import type { User } from '../models'; @@ -1109,6 +1113,54 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati options: localVarRequestOptions, }; }, + /** + * Rebuild specified types of indexes for a document + * @summary Rebuild document indexes + * @param {string} collectionId + * @param {string} documentId + * @param {RebuildIndexesRequest} rebuildIndexesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost: async (collectionId: string, documentId: string, rebuildIndexesRequest: RebuildIndexesRequest, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'collectionId' is not null or undefined + assertParamExists('collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost', 'collectionId', collectionId) + // verify required parameter 'documentId' is not null or undefined + assertParamExists('collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost', 'documentId', documentId) + // verify required parameter 'rebuildIndexesRequest' is not null or undefined + assertParamExists('collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost', 'rebuildIndexesRequest', rebuildIndexesRequest) + const localVarPath = `/collections/{collection_id}/documents/{document_id}/rebuild_indexes` + .replace(`{${"collection_id"}}`, encodeURIComponent(String(collectionId))) + .replace(`{${"document_id"}}`, encodeURIComponent(String(documentId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication BearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(rebuildIndexesRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * Get a list of documents * @summary List documents @@ -2530,6 +2582,21 @@ export const DefaultApiFp = function(configuration?: Configuration) { const localVarOperationServerBasePath = operationServerMap['DefaultApi.collectionsCollectionIdDocumentsDocumentIdPut']?.[localVarOperationServerIndex]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); }, + /** + * Rebuild specified types of indexes for a document + * @summary Rebuild document indexes + * @param {string} collectionId + * @param {string} documentId + * @param {RebuildIndexesRequest} rebuildIndexesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost(collectionId: string, documentId: string, rebuildIndexesRequest: RebuildIndexesRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost(collectionId, documentId, rebuildIndexesRequest, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['DefaultApi.collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, /** * Get a list of documents * @summary List documents @@ -3163,6 +3230,16 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa collectionsCollectionIdDocumentsDocumentIdPut(requestParameters: DefaultApiCollectionsCollectionIdDocumentsDocumentIdPutRequest, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.collectionsCollectionIdDocumentsDocumentIdPut(requestParameters.collectionId, requestParameters.documentId, requestParameters.documentUpdate, options).then((request) => request(axios, basePath)); }, + /** + * Rebuild specified types of indexes for a document + * @summary Rebuild document indexes + * @param {DefaultApiCollectionsCollectionIdDocumentsDocumentIdRebuildIndexesPostRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost(requestParameters: DefaultApiCollectionsCollectionIdDocumentsDocumentIdRebuildIndexesPostRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost(requestParameters.collectionId, requestParameters.documentId, requestParameters.rebuildIndexesRequest, options).then((request) => request(axios, basePath)); + }, /** * Get a list of documents * @summary List documents @@ -3694,6 +3771,16 @@ export interface DefaultApiInterface { */ collectionsCollectionIdDocumentsDocumentIdPut(requestParameters: DefaultApiCollectionsCollectionIdDocumentsDocumentIdPutRequest, options?: RawAxiosRequestConfig): AxiosPromise; + /** + * Rebuild specified types of indexes for a document + * @summary Rebuild document indexes + * @param {DefaultApiCollectionsCollectionIdDocumentsDocumentIdRebuildIndexesPostRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApiInterface + */ + collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost(requestParameters: DefaultApiCollectionsCollectionIdDocumentsDocumentIdRebuildIndexesPostRequest, options?: RawAxiosRequestConfig): AxiosPromise; + /** * Get a list of documents * @summary List documents @@ -4434,6 +4521,34 @@ export interface DefaultApiCollectionsCollectionIdDocumentsDocumentIdPutRequest readonly documentUpdate: DocumentUpdate } +/** + * Request parameters for collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost operation in DefaultApi. + * @export + * @interface DefaultApiCollectionsCollectionIdDocumentsDocumentIdRebuildIndexesPostRequest + */ +export interface DefaultApiCollectionsCollectionIdDocumentsDocumentIdRebuildIndexesPostRequest { + /** + * + * @type {string} + * @memberof DefaultApiCollectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost + */ + readonly collectionId: string + + /** + * + * @type {string} + * @memberof DefaultApiCollectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost + */ + readonly documentId: string + + /** + * + * @type {RebuildIndexesRequest} + * @memberof DefaultApiCollectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost + */ + readonly rebuildIndexesRequest: RebuildIndexesRequest +} + /** * Request parameters for collectionsCollectionIdDocumentsGet operation in DefaultApi. * @export @@ -5134,6 +5249,18 @@ export class DefaultApi extends BaseAPI implements DefaultApiInterface { return DefaultApiFp(this.configuration).collectionsCollectionIdDocumentsDocumentIdPut(requestParameters.collectionId, requestParameters.documentId, requestParameters.documentUpdate, options).then((request) => request(this.axios, this.basePath)); } + /** + * Rebuild specified types of indexes for a document + * @summary Rebuild document indexes + * @param {DefaultApiCollectionsCollectionIdDocumentsDocumentIdRebuildIndexesPostRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost(requestParameters: DefaultApiCollectionsCollectionIdDocumentsDocumentIdRebuildIndexesPostRequest, options?: RawAxiosRequestConfig) { + return DefaultApiFp(this.configuration).collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost(requestParameters.collectionId, requestParameters.documentId, requestParameters.rebuildIndexesRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * Get a list of documents * @summary List documents diff --git a/frontend/src/api/models/index.ts b/frontend/src/api/models/index.ts index 08172cf23..6a02e35ba 100644 --- a/frontend/src/api/models/index.ts +++ b/frontend/src/api/models/index.ts @@ -76,6 +76,7 @@ export * from './node-position'; export * from './page-result'; export * from './prompt-template'; export * from './prompt-template-list'; +export * from './rebuild-indexes-request'; export * from './reference'; export * from './register'; export * from './rerank-document'; @@ -90,6 +91,7 @@ export * from './search-request'; export * from './search-result'; export * from './search-result-item'; export * from './search-result-list'; +export * from './success-response'; export * from './tag-filter-condition'; export * from './tag-filter-request'; export * from './user'; diff --git a/frontend/src/api/models/rebuild-indexes-request.ts b/frontend/src/api/models/rebuild-indexes-request.ts new file mode 100644 index 000000000..811139211 --- /dev/null +++ b/frontend/src/api/models/rebuild-indexes-request.ts @@ -0,0 +1,39 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * ApeRAG API + * ApeRAG API Documentation + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface RebuildIndexesRequest + */ +export interface RebuildIndexesRequest { + /** + * Types of indexes to rebuild + * @type {Array} + * @memberof RebuildIndexesRequest + */ + 'index_types': Array; +} + +export const RebuildIndexesRequestIndexTypesEnum = { + vector: 'vector', + fulltext: 'fulltext', + graph: 'graph' +} as const; + +export type RebuildIndexesRequestIndexTypesEnum = typeof RebuildIndexesRequestIndexTypesEnum[keyof typeof RebuildIndexesRequestIndexTypesEnum]; + + diff --git a/frontend/src/api/models/success-response.ts b/frontend/src/api/models/success-response.ts new file mode 100644 index 000000000..a9b1f9b48 --- /dev/null +++ b/frontend/src/api/models/success-response.ts @@ -0,0 +1,36 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * ApeRAG API + * ApeRAG API Documentation + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface SuccessResponse + */ +export interface SuccessResponse { + /** + * Success code + * @type {string} + * @memberof SuccessResponse + */ + 'code'?: string; + /** + * Success message + * @type {string} + * @memberof SuccessResponse + */ + 'message'?: string; +} + diff --git a/frontend/src/api/openapi.merged.yaml b/frontend/src/api/openapi.merged.yaml index e2e49be48..94e3f7149 100644 --- a/frontend/src/api/openapi.merged.yaml +++ b/frontend/src/api/openapi.merged.yaml @@ -686,6 +686,48 @@ paths: application/json: schema: $ref: '#/components/schemas/failResponse' + /collections/{collection_id}/documents/{document_id}/rebuild_indexes: + post: + summary: Rebuild document indexes + description: Rebuild specified types of indexes for a document + security: + - BearerAuth: [] + parameters: + - name: collection_id + in: path + required: true + schema: + type: string + - name: document_id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/rebuildIndexesRequest' + responses: + '200': + description: Index rebuild initiated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/successResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/failResponse' + '404': + description: Document not found + content: + application/json: + schema: + $ref: '#/components/schemas/failResponse' /collections/{collection_id}/searches: get: summary: Get search history @@ -2795,6 +2837,31 @@ components: type: string source: type: string + rebuildIndexesRequest: + type: object + properties: + index_types: + type: array + items: + type: string + enum: + - vector + - fulltext + - graph + description: Types of indexes to rebuild + required: + - index_types + successResponse: + type: object + properties: + code: + type: string + description: Success code + example: '200' + message: + type: string + description: Success message + example: Operation completed successfully vectorSearchParams: type: object properties: diff --git a/frontend/src/locales/en-US.ts b/frontend/src/locales/en-US.ts index bb7eb6031..3eb0089db 100644 --- a/frontend/src/locales/en-US.ts +++ b/frontend/src/locales/en-US.ts @@ -230,6 +230,13 @@ export default { 'document.index.type.vector': 'Vector Index', 'document.index.type.fulltext': 'Fulltext Index', 'document.index.type.graph': 'Graph Index', + 'document.index.rebuild': 'Rebuild Index', + 'document.index.rebuild.title': 'Rebuild Index', + 'document.index.rebuild.description': 'Select index types to rebuild', + 'document.index.rebuild.select.all': 'Select All', + 'document.index.rebuild.confirm': 'Confirm Rebuild', + 'document.index.rebuild.success': 'Index rebuild task initiated', + 'document.index.rebuild.failed': 'Index rebuild failed', flow: '---------------', 'flow.name': 'Workflow', diff --git a/frontend/src/locales/zh-CN.ts b/frontend/src/locales/zh-CN.ts index 78f3898bf..fbe8b8667 100644 --- a/frontend/src/locales/zh-CN.ts +++ b/frontend/src/locales/zh-CN.ts @@ -228,6 +228,13 @@ export default { 'document.index.type.vector': '向量索引', 'document.index.type.fulltext': '全文索引', 'document.index.type.graph': '图索引', + 'document.index.rebuild': '重建索引', + 'document.index.rebuild.title': '重建索引', + 'document.index.rebuild.description': '选择要重建的索引类型', + 'document.index.rebuild.select.all': '全选', + 'document.index.rebuild.confirm': '确认重建', + 'document.index.rebuild.success': '索引重建任务已启动', + 'document.index.rebuild.failed': '索引重建失败', flow: '---------------', 'flow.name': '任务流', diff --git a/frontend/src/pages/collections/$collectionId/documents.tsx b/frontend/src/pages/collections/$collectionId/documents.tsx index fb943b315..a08fb93b6 100644 --- a/frontend/src/pages/collections/$collectionId/documents.tsx +++ b/frontend/src/pages/collections/$collectionId/documents.tsx @@ -20,12 +20,14 @@ import { DeleteOutlined, MoreOutlined, SearchOutlined, + ReloadOutlined, } from '@ant-design/icons'; import { useRequest } from 'ahooks'; import { Avatar, Badge, Button, + Checkbox, Dropdown, Input, Modal, @@ -56,6 +58,9 @@ export default () => { const { token } = theme.useToken(); const [modal, contextHolder] = Modal.useModal(); const { formatMessage } = useIntl(); + const [rebuildModalVisible, setRebuildModalVisible] = useState(false); + const [rebuildSelectedDocument, setRebuildSelectedDocument] = useState(null); + const [rebuildSelectedTypes, setRebuildSelectedTypes] = useState([]); const { data: documentsRes, run: getDocuments, @@ -86,6 +91,48 @@ export default () => { [collectionId], ); + const rebuildIndexes = useCallback( + async (documentId: string, indexTypes: string[]) => { + if (!collectionId || !documentId || indexTypes.length === 0) return; + + try { + await api.collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost({ + collectionId, + documentId, + rebuildIndexesRequest: { + index_types: indexTypes as any, + }, + }); + toast.success(formatMessage({ id: 'document.index.rebuild.success' })); + getDocuments(); + } catch (error) { + toast.error(formatMessage({ id: 'document.index.rebuild.failed' })); + } + }, + [collectionId, formatMessage], + ); + + const handleRebuildIndex = useCallback((record: ApeDocument) => { + setRebuildSelectedDocument(record); + setRebuildSelectedTypes([]); + setRebuildModalVisible(true); + }, []); + + const handleRebuildConfirm = useCallback(() => { + if (rebuildSelectedDocument && rebuildSelectedTypes.length > 0) { + rebuildIndexes(rebuildSelectedDocument.id!, rebuildSelectedTypes); + setRebuildModalVisible(false); + setRebuildSelectedDocument(null); + setRebuildSelectedTypes([]); + } + }, [rebuildSelectedDocument, rebuildSelectedTypes, rebuildIndexes]); + + const indexTypeOptions = [ + { label: formatMessage({ id: 'document.index.type.vector' }), value: 'vector' }, + { label: formatMessage({ id: 'document.index.type.fulltext' }), value: 'fulltext' }, + { label: formatMessage({ id: 'document.index.type.graph' }), value: 'graph' }, + ]; + const renderIndexStatus = ( vectorStatus?: DocumentVectorIndexStatusEnum, fulltextStatus?: DocumentFulltextIndexStatusEnum, @@ -206,6 +253,13 @@ export default () => { trigger={['click']} menu={{ items: [ + { + key: 'rebuild', + label: formatMessage({ id: 'document.index.rebuild' }), + icon: , + disabled: record.status === 'DELETING' || record.status === 'DELETED', + onClick: () => handleRebuildIndex(record), + }, { key: 'delete', label: formatMessage({ id: 'action.delete' }), @@ -273,15 +327,15 @@ export default () => { const documents = useMemo( () => - documentsRes?.data.items - ?.map((document) => { + documentsRes?.data?.items + ?.map((document: any) => { const item: ApeDocument = { ...document, config: parseConfig(document.config), }; return item; }) - .filter((item) => { + .filter((item: ApeDocument) => { const titleMatch = searchParams?.name ? item.name?.includes(searchParams.name) : true; @@ -330,6 +384,59 @@ export default () => { {contextHolder} + + { + setRebuildModalVisible(false); + setRebuildSelectedDocument(null); + setRebuildSelectedTypes([]); + }} + onOk={handleRebuildConfirm} + okText={formatMessage({ id: 'document.index.rebuild.confirm' })} + cancelText={formatMessage({ id: 'action.cancel' })} + okButtonProps={{ + disabled: rebuildSelectedTypes.length === 0, + }} + > +
+ + {formatMessage({ id: 'document.index.rebuild.description' })} + +
+ + {rebuildSelectedDocument && ( +
+ + {rebuildSelectedDocument.name} + +
+ )} + +
+ 0 && rebuildSelectedTypes.length < indexTypeOptions.length} + checked={rebuildSelectedTypes.length === indexTypeOptions.length} + onChange={(e) => { + if (e.target.checked) { + setRebuildSelectedTypes(indexTypeOptions.map(option => option.value)); + } else { + setRebuildSelectedTypes([]); + } + }} + > + {formatMessage({ id: 'document.index.rebuild.select.all' })} + +
+ + setRebuildSelectedTypes(values as string[])} + style={{ display: 'flex', flexDirection: 'column', gap: 8 }} + /> +
); }; From 4ba012734d8209f86cf85be5d36d855997c8307e Mon Sep 17 00:00:00 2001 From: Guo Ziang Date: Mon, 23 Jun 2025 15:50:47 +0800 Subject: [PATCH 2/6] feat: implement rebuild indexes functionality for documents - Added a new API endpoint to rebuild specified types of indexes for a document. - Introduced request and response models for the rebuild indexes feature. - Updated the document service to handle index rebuilding logic. - Enhanced the main view with a new endpoint for triggering index rebuilds. - Included logging for index rebuild operations and success responses. --- aperag/api/components/schemas/common.yaml | 12 +++++ aperag/api/components/schemas/document.yaml | 15 ++++++ aperag/api/openapi.yaml | 2 + aperag/api/paths/collections.yaml | 43 ++++++++++++++++ aperag/index/manager.py | 31 +++++++++++ aperag/schema/view_models.py | 15 +++++- aperag/service/document_service.py | 57 +++++++++++++++++++++ aperag/views/main.py | 15 ++++++ 8 files changed, 189 insertions(+), 1 deletion(-) diff --git a/aperag/api/components/schemas/common.yaml b/aperag/api/components/schemas/common.yaml index 4c0196787..9d0e1ca83 100644 --- a/aperag/api/components/schemas/common.yaml +++ b/aperag/api/components/schemas/common.yaml @@ -23,3 +23,15 @@ pageResult: type: integer description: The total count of items type: object + +successResponse: + type: object + properties: + code: + type: string + description: Success code + example: "200" + message: + type: string + description: Success message + example: "Operation completed successfully" diff --git a/aperag/api/components/schemas/document.yaml b/aperag/api/components/schemas/document.yaml index 361a4b587..8485469f5 100644 --- a/aperag/api/components/schemas/document.yaml +++ b/aperag/api/components/schemas/document.yaml @@ -82,3 +82,18 @@ documentUpdate: type: string source: type: string + +rebuildIndexesRequest: + type: object + properties: + index_types: + type: array + items: + type: string + enum: + - vector + - fulltext + - graph + description: Types of indexes to rebuild + required: + - index_types diff --git a/aperag/api/openapi.yaml b/aperag/api/openapi.yaml index 4b0a1120e..8047a2610 100644 --- a/aperag/api/openapi.yaml +++ b/aperag/api/openapi.yaml @@ -47,6 +47,8 @@ paths: $ref: './paths/collections.yaml#/documents' /collections/{collection_id}/documents/{document_id}: $ref: './paths/collections.yaml#/document' + /collections/{collection_id}/documents/{document_id}/rebuild_indexes: + $ref: './paths/collections.yaml#/rebuild_indexes' /collections/{collection_id}/searches: $ref: './paths/collections.yaml#/searches' /collections/{collection_id}/searches/{search_id}: diff --git a/aperag/api/paths/collections.yaml b/aperag/api/paths/collections.yaml index 2a0ca9bd8..121fb2cb9 100644 --- a/aperag/api/paths/collections.yaml +++ b/aperag/api/paths/collections.yaml @@ -296,6 +296,49 @@ document: schema: $ref: '../components/schemas/common.yaml#/failResponse' +rebuild_indexes: + post: + summary: Rebuild document indexes + description: Rebuild specified types of indexes for a document + security: + - BearerAuth: [] + parameters: + - name: collection_id + in: path + required: true + schema: + type: string + - name: document_id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '../components/schemas/document.yaml#/rebuildIndexesRequest' + responses: + '200': + description: Index rebuild initiated successfully + content: + application/json: + schema: + $ref: '../components/schemas/common.yaml#/successResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '../components/schemas/common.yaml#/failResponse' + '404': + description: Document not found + content: + application/json: + schema: + $ref: '../components/schemas/common.yaml#/failResponse' + searches: get: summary: Get search history diff --git a/aperag/index/manager.py b/aperag/index/manager.py index 3b8a0219c..90a51f1f2 100644 --- a/aperag/index/manager.py +++ b/aperag/index/manager.py @@ -107,6 +107,37 @@ async def delete_document_indexes( if doc_index: doc_index.update_spec(IndexDesiredState.ABSENT) + async def rebuild_document_indexes( + self, session: AsyncSession, document_id: str, index_types: List[DocumentIndexType] + ): + """ + Rebuild specified document indexes (called when user requests index rebuild) + + This increments the version of specified indexes to trigger reconciliation. + + Args: + session: Database session + document_id: Document ID + index_types: List of index types to rebuild + """ + for index_type in index_types: + stmt = select(DocumentIndex).where( + and_(DocumentIndex.document_id == document_id, DocumentIndex.index_type == index_type) + ) + result = await session.execute(stmt) + doc_index = result.scalar_one_or_none() + + if doc_index: + # Only rebuild if the index is present or failed + if doc_index.desired_state == IndexDesiredState.PRESENT: + doc_index.version += 1 # Increment version to trigger re-indexing + doc_index.gmt_updated = utc_now() + logger.info(f"Triggered rebuild for {index_type.value} index of document {document_id}") + else: + logger.warning(f"Cannot rebuild {index_type.value} index for document {document_id}: index not present") + else: + logger.warning(f"No {index_type.value} index found for document {document_id}") + async def get_document_index_status(self, session: AsyncSession, document_id: str) -> dict: """ Get current index status for a document diff --git a/aperag/schema/view_models.py b/aperag/schema/view_models.py index 993469ab0..2e39b8f83 100644 --- a/aperag/schema/view_models.py +++ b/aperag/schema/view_models.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: openapi.merged.yaml -# timestamp: 2025-06-23T03:26:47+00:00 +# timestamp: 2025-06-23T07:36:11+00:00 from __future__ import annotations @@ -575,6 +575,19 @@ class DocumentUpdate(BaseModel): source: Optional[str] = None +class RebuildIndexesRequest(BaseModel): + index_types: list[Literal['vector', 'fulltext', 'graph']] = Field( + ..., description='Types of indexes to rebuild' + ) + + +class SuccessResponse(BaseModel): + code: Optional[str] = Field(None, description='Success code', example='200') + message: Optional[str] = Field( + None, description='Success message', example='Operation completed successfully' + ) + + class VectorSearchParams(BaseModel): topk: Optional[int] = Field(None, description='Top K results') similarity: Optional[confloat(ge=0.0, le=1.0)] = Field( diff --git a/aperag/service/document_service.py b/aperag/service/document_service.py index 380ba926e..8d05d5b90 100644 --- a/aperag/service/document_service.py +++ b/aperag/service/document_service.py @@ -402,6 +402,63 @@ async def _delete_documents_atomically(session): return result + async def rebuild_document_indexes( + self, user_id: str, collection_id: str, document_id: str, index_types: List[str] + ) -> dict: + """ + Rebuild specified indexes for a document + + Args: + user_id: User ID + collection_id: Collection ID + document_id: Document ID + index_types: List of index types to rebuild ('vector', 'fulltext', 'graph') + + Returns: + dict: Success response + """ + logger.info(f"Rebuilding indexes for document {document_id} with types: {index_types}") + + async for session in get_async_session(): + # Verify document exists and user has access + document = await self.db_ops.query_document(user_id, collection_id, document_id) + if not document: + raise DocumentNotFoundException(f"Document {document_id} not found") + + if document.collection_id != collection_id: + raise ResourceNotFoundException(f"Document {document_id} not found in collection {collection_id}") + + # Verify user has access to the collection + collection = await self.db_ops.query_collection(user_id, collection_id) + if not collection or collection.user != user_id: + raise ResourceNotFoundException(f"Collection {collection_id} not found or access denied") + + # Convert index types to enum values + from aperag.db.models import DocumentIndexType + index_type_enums = [] + for index_type in index_types: + if index_type == 'vector': + index_type_enums.append(DocumentIndexType.VECTOR) + elif index_type == 'fulltext': + index_type_enums.append(DocumentIndexType.FULLTEXT) + elif index_type == 'graph': + index_type_enums.append(DocumentIndexType.GRAPH) + else: + raise invalid_param(f"Invalid index type: {index_type}") + + # Trigger index rebuild by incrementing version for selected index types + await document_index_manager.rebuild_document_indexes(session, document_id, index_type_enums) + await session.commit() + + logger.info(f"Successfully triggered rebuild for document {document_id} indexes: {index_types}") + + _trigger_index_reconciliation() + + return { + "code": "200", + "message": f"Index rebuild initiated for types: {', '.join(index_types)}" + } + # Create a global service instance for easy access # This uses the global db_ops instance and doesn't require session management in views diff --git a/aperag/views/main.py b/aperag/views/main.py index cd26fab37..ebb13ea63 100644 --- a/aperag/views/main.py +++ b/aperag/views/main.py @@ -158,6 +158,21 @@ async def delete_documents_view( return await document_service.delete_documents(str(user.id), collection_id, document_ids) +@router.post("/collections/{collection_id}/documents/{document_id}/rebuild_indexes") +@audit(resource_type="document", api_name="RebuildDocumentIndexes") +async def rebuild_document_indexes_view( + request: Request, + collection_id: str, + document_id: str, + rebuild_request: view_models.RebuildIndexesRequest, + user: User = Depends(current_user), +): + """Rebuild specified indexes for a document""" + return await document_service.rebuild_document_indexes( + str(user.id), collection_id, document_id, rebuild_request.index_types + ) + + @router.post("/bots/{bot_id}/chats") @audit(resource_type="chat", api_name="CreateChat") async def create_chat_view(request: Request, bot_id: str, user: User = Depends(current_user)) -> view_models.Chat: From 34d97b7152dc28ba963499a8e2d94f5ce7bd97c7 Mon Sep 17 00:00:00 2001 From: Guo Ziang Date: Mon, 23 Jun 2025 16:34:46 +0800 Subject: [PATCH 3/6] chore: tidy up --- Makefile | 10 ++++++ aperag/api/components/schemas/common.yaml | 12 ------- aperag/api/paths/collections.yaml | 6 +--- aperag/schema/view_models.py | 9 +---- frontend/src/api/apis/default-api.ts | 8 ++--- frontend/src/api/models/index.ts | 1 - frontend/src/api/models/success-response.ts | 36 ------------------- frontend/src/api/openapi.merged.yaml | 17 +-------- .../collections/$collectionId/documents.tsx | 2 +- 9 files changed, 17 insertions(+), 84 deletions(-) delete mode 100644 frontend/src/api/models/success-response.ts diff --git a/Makefile b/Makefile index 7166e6a69..0dac6a30d 100644 --- a/Makefile +++ b/Makefile @@ -176,6 +176,7 @@ merge-openapi: @cd aperag && redocly bundle ./api/openapi.yaml > ./api/openapi.merged.yaml generate-models: merge-openapi + @echo "Generating backend models..." @datamodel-codegen \ --input aperag/api/openapi.merged.yaml \ --input-file-type openapi \ @@ -185,6 +186,15 @@ generate-models: merge-openapi --use-standard-collections \ --use-schema-description \ --enum-field-as-literal all + @echo "Generating frontend SDK..." + @openapi-generator-cli generate \ + --input-spec="aperag/api/openapi.merged.yaml" \ + --generator-name="typescript-axios" \ + --output="frontend/src/api" \ + --api-package="apis" \ + --model-package="models" \ + --model-name-prefix="" \ + --skip-validate-spec --additional-properties="enumPropertyNaming=original,supportsES6=true,withInterfaces=true,prependFormOrBodyParameters=false,withoutPrefixEnums=false,withSeparateModelsAndApi=true,useSingleRequestParameter=true" @rm aperag/api/openapi.merged.yaml generate-frontend-sdk: diff --git a/aperag/api/components/schemas/common.yaml b/aperag/api/components/schemas/common.yaml index 9d0e1ca83..4c0196787 100644 --- a/aperag/api/components/schemas/common.yaml +++ b/aperag/api/components/schemas/common.yaml @@ -23,15 +23,3 @@ pageResult: type: integer description: The total count of items type: object - -successResponse: - type: object - properties: - code: - type: string - description: Success code - example: "200" - message: - type: string - description: Success message - example: "Operation completed successfully" diff --git a/aperag/api/paths/collections.yaml b/aperag/api/paths/collections.yaml index 121fb2cb9..58e738af0 100644 --- a/aperag/api/paths/collections.yaml +++ b/aperag/api/paths/collections.yaml @@ -320,12 +320,8 @@ rebuild_indexes: schema: $ref: '../components/schemas/document.yaml#/rebuildIndexesRequest' responses: - '200': + '204': description: Index rebuild initiated successfully - content: - application/json: - schema: - $ref: '../components/schemas/common.yaml#/successResponse' '401': description: Unauthorized content: diff --git a/aperag/schema/view_models.py b/aperag/schema/view_models.py index 2e39b8f83..6ee38a7e2 100644 --- a/aperag/schema/view_models.py +++ b/aperag/schema/view_models.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: openapi.merged.yaml -# timestamp: 2025-06-23T07:36:11+00:00 +# timestamp: 2025-06-23T08:28:25+00:00 from __future__ import annotations @@ -581,13 +581,6 @@ class RebuildIndexesRequest(BaseModel): ) -class SuccessResponse(BaseModel): - code: Optional[str] = Field(None, description='Success code', example='200') - message: Optional[str] = Field( - None, description='Success message', example='Operation completed successfully' - ) - - class VectorSearchParams(BaseModel): topk: Optional[int] = Field(None, description='Top K results') similarity: Optional[confloat(ge=0.0, le=1.0)] = Field( diff --git a/frontend/src/api/apis/default-api.ts b/frontend/src/api/apis/default-api.ts index 2aaab76c3..d569bc7eb 100644 --- a/frontend/src/api/apis/default-api.ts +++ b/frontend/src/api/apis/default-api.ts @@ -112,8 +112,6 @@ import type { SearchResult } from '../models'; // @ts-ignore import type { SearchResultList } from '../models'; // @ts-ignore -import type { SuccessResponse } from '../models'; -// @ts-ignore import type { TagFilterRequest } from '../models'; // @ts-ignore import type { User } from '../models'; @@ -2591,7 +2589,7 @@ export const DefaultApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost(collectionId: string, documentId: string, rebuildIndexesRequest: RebuildIndexesRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost(collectionId: string, documentId: string, rebuildIndexesRequest: RebuildIndexesRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost(collectionId, documentId, rebuildIndexesRequest, options); const localVarOperationServerIndex = configuration?.serverIndex ?? 0; const localVarOperationServerBasePath = operationServerMap['DefaultApi.collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost']?.[localVarOperationServerIndex]?.url; @@ -3237,7 +3235,7 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa * @param {*} [options] Override http request option. * @throws {RequiredError} */ - collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost(requestParameters: DefaultApiCollectionsCollectionIdDocumentsDocumentIdRebuildIndexesPostRequest, options?: RawAxiosRequestConfig): AxiosPromise { + collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost(requestParameters: DefaultApiCollectionsCollectionIdDocumentsDocumentIdRebuildIndexesPostRequest, options?: RawAxiosRequestConfig): AxiosPromise { return localVarFp.collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost(requestParameters.collectionId, requestParameters.documentId, requestParameters.rebuildIndexesRequest, options).then((request) => request(axios, basePath)); }, /** @@ -3779,7 +3777,7 @@ export interface DefaultApiInterface { * @throws {RequiredError} * @memberof DefaultApiInterface */ - collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost(requestParameters: DefaultApiCollectionsCollectionIdDocumentsDocumentIdRebuildIndexesPostRequest, options?: RawAxiosRequestConfig): AxiosPromise; + collectionsCollectionIdDocumentsDocumentIdRebuildIndexesPost(requestParameters: DefaultApiCollectionsCollectionIdDocumentsDocumentIdRebuildIndexesPostRequest, options?: RawAxiosRequestConfig): AxiosPromise; /** * Get a list of documents diff --git a/frontend/src/api/models/index.ts b/frontend/src/api/models/index.ts index 6a02e35ba..ea46d69a5 100644 --- a/frontend/src/api/models/index.ts +++ b/frontend/src/api/models/index.ts @@ -91,7 +91,6 @@ export * from './search-request'; export * from './search-result'; export * from './search-result-item'; export * from './search-result-list'; -export * from './success-response'; export * from './tag-filter-condition'; export * from './tag-filter-request'; export * from './user'; diff --git a/frontend/src/api/models/success-response.ts b/frontend/src/api/models/success-response.ts deleted file mode 100644 index a9b1f9b48..000000000 --- a/frontend/src/api/models/success-response.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * ApeRAG API - * ApeRAG API Documentation - * - * The version of the OpenAPI document: 1.0.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - - -/** - * - * @export - * @interface SuccessResponse - */ -export interface SuccessResponse { - /** - * Success code - * @type {string} - * @memberof SuccessResponse - */ - 'code'?: string; - /** - * Success message - * @type {string} - * @memberof SuccessResponse - */ - 'message'?: string; -} - diff --git a/frontend/src/api/openapi.merged.yaml b/frontend/src/api/openapi.merged.yaml index 94e3f7149..a2176db17 100644 --- a/frontend/src/api/openapi.merged.yaml +++ b/frontend/src/api/openapi.merged.yaml @@ -710,12 +710,8 @@ paths: schema: $ref: '#/components/schemas/rebuildIndexesRequest' responses: - '200': + '204': description: Index rebuild initiated successfully - content: - application/json: - schema: - $ref: '#/components/schemas/successResponse' '401': description: Unauthorized content: @@ -2851,17 +2847,6 @@ components: description: Types of indexes to rebuild required: - index_types - successResponse: - type: object - properties: - code: - type: string - description: Success code - example: '200' - message: - type: string - description: Success message - example: Operation completed successfully vectorSearchParams: type: object properties: diff --git a/frontend/src/pages/collections/$collectionId/documents.tsx b/frontend/src/pages/collections/$collectionId/documents.tsx index a08fb93b6..8db28f53e 100644 --- a/frontend/src/pages/collections/$collectionId/documents.tsx +++ b/frontend/src/pages/collections/$collectionId/documents.tsx @@ -434,7 +434,7 @@ export default () => { options={indexTypeOptions} value={rebuildSelectedTypes} onChange={(values) => setRebuildSelectedTypes(values as string[])} - style={{ display: 'flex', flexDirection: 'column', gap: 8 }} + style={{ display: 'flex', flexDirection: 'row', gap: 16 }} /> From 8932620943c5604257d941316639609d9143c6d0 Mon Sep 17 00:00:00 2001 From: Guo Ziang Date: Mon, 23 Jun 2025 16:41:14 +0800 Subject: [PATCH 4/6] chore: fix bug --- aperag/service/document_service.py | 45 +++++++++++++++++------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/aperag/service/document_service.py b/aperag/service/document_service.py index 8d05d5b90..1fc94fa70 100644 --- a/aperag/service/document_service.py +++ b/aperag/service/document_service.py @@ -419,7 +419,21 @@ async def rebuild_document_indexes( """ logger.info(f"Rebuilding indexes for document {document_id} with types: {index_types}") - async for session in get_async_session(): + # Convert index types to enum values outside transaction + from aperag.db.models import DocumentIndexType + index_type_enums = [] + for index_type in index_types: + if index_type == 'vector': + index_type_enums.append(DocumentIndexType.VECTOR) + elif index_type == 'fulltext': + index_type_enums.append(DocumentIndexType.FULLTEXT) + elif index_type == 'graph': + index_type_enums.append(DocumentIndexType.GRAPH) + else: + raise invalid_param("index_type", f"Invalid index type: {index_type}") + + # Execute all operations atomically in a single transaction + async def _rebuild_document_indexes_atomically(session): # Verify document exists and user has access document = await self.db_ops.query_document(user_id, collection_id, document_id) if not document: @@ -433,31 +447,22 @@ async def rebuild_document_indexes( if not collection or collection.user != user_id: raise ResourceNotFoundException(f"Collection {collection_id} not found or access denied") - # Convert index types to enum values - from aperag.db.models import DocumentIndexType - index_type_enums = [] - for index_type in index_types: - if index_type == 'vector': - index_type_enums.append(DocumentIndexType.VECTOR) - elif index_type == 'fulltext': - index_type_enums.append(DocumentIndexType.FULLTEXT) - elif index_type == 'graph': - index_type_enums.append(DocumentIndexType.GRAPH) - else: - raise invalid_param(f"Invalid index type: {index_type}") - # Trigger index rebuild by incrementing version for selected index types await document_index_manager.rebuild_document_indexes(session, document_id, index_type_enums) - await session.commit() - + logger.info(f"Successfully triggered rebuild for document {document_id} indexes: {index_types}") + + return { + "code": "200", + "message": f"Index rebuild initiated for types: {', '.join(index_types)}" + } + result = await self.db_ops.execute_with_transaction(_rebuild_document_indexes_atomically) + + # Trigger index reconciliation after successful rebuild initiation _trigger_index_reconciliation() - return { - "code": "200", - "message": f"Index rebuild initiated for types: {', '.join(index_types)}" - } + return result # Create a global service instance for easy access From 7253e675e90fd68ca277d646c6f17e9218ecb792 Mon Sep 17 00:00:00 2001 From: Guo Ziang Date: Mon, 23 Jun 2025 17:13:05 +0800 Subject: [PATCH 5/6] chore: tidy up --- aperag/index/manager.py | 3 + aperag/service/document_service.py | 3 + tests/e2e_test/test_document.py | 111 +++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+) diff --git a/aperag/index/manager.py b/aperag/index/manager.py index 90a51f1f2..7bcf8cbdd 100644 --- a/aperag/index/manager.py +++ b/aperag/index/manager.py @@ -120,6 +120,9 @@ async def rebuild_document_indexes( document_id: Document ID index_types: List of index types to rebuild """ + if len(set(index_types)) != len(index_types): + raise Exception("Duplicate index types are not allowed") + for index_type in index_types: stmt = select(DocumentIndex).where( and_(DocumentIndex.document_id == document_id, DocumentIndex.index_type == index_type) diff --git a/aperag/service/document_service.py b/aperag/service/document_service.py index 1fc94fa70..96e2c4619 100644 --- a/aperag/service/document_service.py +++ b/aperag/service/document_service.py @@ -417,6 +417,9 @@ async def rebuild_document_indexes( Returns: dict: Success response """ + if len(set(index_types)) != len(index_types): + raise invalid_param("index_types", "duplicate index types are not allowed") + logger.info(f"Rebuilding indexes for document {document_id} with types: {index_types}") # Convert index types to enum values outside transaction diff --git a/tests/e2e_test/test_document.py b/tests/e2e_test/test_document.py index 8c3886468..d0f64e82e 100644 --- a/tests/e2e_test/test_document.py +++ b/tests/e2e_test/test_document.py @@ -129,3 +129,114 @@ def test_upload_duplicate_then_delete_and_reupload(client, collection): assert "items" in data3 assert len(data3["items"]) == 1 assert data3["items"][0]["name"] == filename + + +# Document index rebuild tests +def test_rebuild_single_index_type(client, document, collection): + """Test rebuilding a single index type (vector)""" + doc_id = document["id"] + collection_id = collection["id"] + + # Test rebuilding vector index + rebuild_request = {"index_types": ["vector"]} + response = client.post( + f"/api/v1/collections/{collection_id}/documents/{doc_id}/rebuild_indexes", + json=rebuild_request + ) + assert response.status_code == HTTPStatus.OK, response.text + data = response.json() + assert data["code"] == "200" + assert "vector" in data["message"] + + +def test_rebuild_all_index_types(client, document, collection): + """Test rebuilding all supported index types""" + doc_id = document["id"] + collection_id = collection["id"] + + # Test rebuilding all index types + rebuild_request = {"index_types": ["vector", "fulltext", "graph"]} + response = client.post( + f"/api/v1/collections/{collection_id}/documents/{doc_id}/rebuild_indexes", + json=rebuild_request + ) + assert response.status_code == HTTPStatus.OK, response.text + data = response.json() + assert data["code"] == "200" + assert "vector" in data["message"] + assert "fulltext" in data["message"] + assert "graph" in data["message"] + + +def test_rebuild_index_invalid_index_type(client, document, collection): + """Test rebuilding with invalid index type""" + doc_id = document["id"] + collection_id = collection["id"] + + # Test with invalid index type + rebuild_request = {"index_types": ["invalid_type"]} + response = client.post( + f"/api/v1/collections/{collection_id}/documents/{doc_id}/rebuild_indexes", + json=rebuild_request + ) + assert response.status_code == HTTPStatus.BAD_REQUEST, response.text + + +def test_rebuild_index_empty_index_types(client, document, collection): + """Test rebuilding with empty index types array""" + doc_id = document["id"] + collection_id = collection["id"] + + # Test with empty index types + rebuild_request = {"index_types": []} + response = client.post( + f"/api/v1/collections/{collection_id}/documents/{doc_id}/rebuild_indexes", + json=rebuild_request + ) + assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY, response.text + + +@pytest.mark.parametrize("index_type", ["vector", "fulltext", "graph"]) +def test_rebuild_individual_index_types(client, document, collection, index_type): + """Test rebuilding each individual index type""" + doc_id = document["id"] + collection_id = collection["id"] + + rebuild_request = {"index_types": [index_type]} + response = client.post( + f"/api/v1/collections/{collection_id}/documents/{doc_id}/rebuild_indexes", + json=rebuild_request + ) + assert response.status_code == HTTPStatus.OK, response.text + data = response.json() + assert data["code"] == "200" + assert index_type in data["message"] + + +def test_rebuild_index_duplicate_types(client, document, collection): + """Test rebuilding with duplicate index types in request""" + doc_id = document["id"] + collection_id = collection["id"] + + # Test with duplicate index types + rebuild_request = {"index_types": ["vector", "vector", "fulltext"]} + response = client.post( + f"/api/v1/collections/{collection_id}/documents/{doc_id}/rebuild_indexes", + json=rebuild_request + ) + # Should fail + assert response.status_code == HTTPStatus.BAD_REQUEST, response.text + + +def test_rebuild_index_case_sensitivity(client, document, collection): + """Test rebuilding with different case index types""" + doc_id = document["id"] + collection_id = collection["id"] + + # Test with uppercase index type (should fail) + rebuild_request = {"index_types": ["VECTOR"]} + response = client.post( + f"/api/v1/collections/{collection_id}/documents/{doc_id}/rebuild_indexes", + json=rebuild_request + ) + assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY, response.text From e380ab046bd6364b32c8194a038567c3e38e57af Mon Sep 17 00:00:00 2001 From: Guo Ziang Date: Mon, 23 Jun 2025 17:51:11 +0800 Subject: [PATCH 6/6] chore: tidy up --- Makefile | 10 ---------- aperag/api/components/schemas/document.yaml | 1 + aperag/schema/view_models.py | 4 ++-- tests/e2e_test/test_document.py | 2 +- 4 files changed, 4 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index 0dac6a30d..7166e6a69 100644 --- a/Makefile +++ b/Makefile @@ -176,7 +176,6 @@ merge-openapi: @cd aperag && redocly bundle ./api/openapi.yaml > ./api/openapi.merged.yaml generate-models: merge-openapi - @echo "Generating backend models..." @datamodel-codegen \ --input aperag/api/openapi.merged.yaml \ --input-file-type openapi \ @@ -186,15 +185,6 @@ generate-models: merge-openapi --use-standard-collections \ --use-schema-description \ --enum-field-as-literal all - @echo "Generating frontend SDK..." - @openapi-generator-cli generate \ - --input-spec="aperag/api/openapi.merged.yaml" \ - --generator-name="typescript-axios" \ - --output="frontend/src/api" \ - --api-package="apis" \ - --model-package="models" \ - --model-name-prefix="" \ - --skip-validate-spec --additional-properties="enumPropertyNaming=original,supportsES6=true,withInterfaces=true,prependFormOrBodyParameters=false,withoutPrefixEnums=false,withSeparateModelsAndApi=true,useSingleRequestParameter=true" @rm aperag/api/openapi.merged.yaml generate-frontend-sdk: diff --git a/aperag/api/components/schemas/document.yaml b/aperag/api/components/schemas/document.yaml index 8485469f5..78a75ff34 100644 --- a/aperag/api/components/schemas/document.yaml +++ b/aperag/api/components/schemas/document.yaml @@ -95,5 +95,6 @@ rebuildIndexesRequest: - fulltext - graph description: Types of indexes to rebuild + minItems: 1 required: - index_types diff --git a/aperag/schema/view_models.py b/aperag/schema/view_models.py index 6ee38a7e2..d91941ae3 100644 --- a/aperag/schema/view_models.py +++ b/aperag/schema/view_models.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: openapi.merged.yaml -# timestamp: 2025-06-23T08:28:25+00:00 +# timestamp: 2025-06-23T09:37:51+00:00 from __future__ import annotations @@ -577,7 +577,7 @@ class DocumentUpdate(BaseModel): class RebuildIndexesRequest(BaseModel): index_types: list[Literal['vector', 'fulltext', 'graph']] = Field( - ..., description='Types of indexes to rebuild' + ..., description='Types of indexes to rebuild', min_items=1 ) diff --git a/tests/e2e_test/test_document.py b/tests/e2e_test/test_document.py index d0f64e82e..b1bde27d0 100644 --- a/tests/e2e_test/test_document.py +++ b/tests/e2e_test/test_document.py @@ -179,7 +179,7 @@ def test_rebuild_index_invalid_index_type(client, document, collection): f"/api/v1/collections/{collection_id}/documents/{doc_id}/rebuild_indexes", json=rebuild_request ) - assert response.status_code == HTTPStatus.BAD_REQUEST, response.text + assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY, response.text def test_rebuild_index_empty_index_types(client, document, collection):