mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-22 23:31:12 +02:00
fix: handle IntegrityError in prepare_for_indexing and add within-batch content dedup test
This commit is contained in:
parent
1b4ed35de3
commit
e6b7ce7345
2 changed files with 89 additions and 56 deletions
|
|
@ -1,13 +1,11 @@
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import delete, select
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from sqlalchemy.orm import object_session
|
from sqlalchemy.orm import object_session
|
||||||
from sqlalchemy.orm.attributes import set_committed_value
|
from sqlalchemy.orm.attributes import set_committed_value
|
||||||
|
|
||||||
from sqlalchemy import delete
|
|
||||||
|
|
||||||
from app.db import Chunk, Document, DocumentStatus
|
from app.db import Chunk, Document, DocumentStatus
|
||||||
from app.indexing_pipeline.connector_document import ConnectorDocument
|
from app.indexing_pipeline.connector_document import ConnectorDocument
|
||||||
from app.indexing_pipeline.document_chunker import chunk_text
|
from app.indexing_pipeline.document_chunker import chunk_text
|
||||||
|
|
@ -39,67 +37,75 @@ class IndexingPipelineService:
|
||||||
"""
|
"""
|
||||||
Persist new documents and detect changes, returning only those that need indexing.
|
Persist new documents and detect changes, returning only those that need indexing.
|
||||||
"""
|
"""
|
||||||
documents = []
|
try:
|
||||||
seen_hashes: set[str] = set()
|
documents = []
|
||||||
|
seen_hashes: set[str] = set()
|
||||||
|
|
||||||
for connector_doc in connector_docs:
|
for connector_doc in connector_docs:
|
||||||
unique_identifier_hash = compute_unique_identifier_hash(connector_doc)
|
unique_identifier_hash = compute_unique_identifier_hash(connector_doc)
|
||||||
content_hash = compute_content_hash(connector_doc)
|
content_hash = compute_content_hash(connector_doc)
|
||||||
|
|
||||||
if unique_identifier_hash in seen_hashes:
|
if unique_identifier_hash in seen_hashes:
|
||||||
continue
|
continue
|
||||||
seen_hashes.add(unique_identifier_hash)
|
seen_hashes.add(unique_identifier_hash)
|
||||||
|
|
||||||
result = await self.session.execute(
|
result = await self.session.execute(
|
||||||
select(Document).filter(Document.unique_identifier_hash == unique_identifier_hash)
|
select(Document).filter(Document.unique_identifier_hash == unique_identifier_hash)
|
||||||
)
|
)
|
||||||
existing = result.scalars().first()
|
existing = result.scalars().first()
|
||||||
|
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
if existing.content_hash == content_hash:
|
if existing.content_hash == content_hash:
|
||||||
if existing.title != connector_doc.title:
|
if existing.title != connector_doc.title:
|
||||||
existing.title = connector_doc.title
|
existing.title = connector_doc.title
|
||||||
existing.updated_at = datetime.now(UTC)
|
existing.updated_at = datetime.now(UTC)
|
||||||
if not DocumentStatus.is_state(existing.status, DocumentStatus.READY):
|
if not DocumentStatus.is_state(existing.status, DocumentStatus.READY):
|
||||||
existing.status = DocumentStatus.pending()
|
existing.status = DocumentStatus.pending()
|
||||||
existing.updated_at = datetime.now(UTC)
|
existing.updated_at = datetime.now(UTC)
|
||||||
documents.append(existing)
|
documents.append(existing)
|
||||||
|
continue
|
||||||
|
|
||||||
|
existing.title = connector_doc.title
|
||||||
|
existing.content_hash = content_hash
|
||||||
|
existing.source_markdown = connector_doc.source_markdown
|
||||||
|
existing.document_metadata = connector_doc.metadata
|
||||||
|
existing.updated_at = datetime.now(UTC)
|
||||||
|
existing.status = DocumentStatus.pending()
|
||||||
|
documents.append(existing)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
existing.title = connector_doc.title
|
duplicate = await self.session.execute(
|
||||||
existing.content_hash = content_hash
|
select(Document).filter(Document.content_hash == content_hash)
|
||||||
existing.source_markdown = connector_doc.source_markdown
|
)
|
||||||
existing.document_metadata = connector_doc.metadata
|
if duplicate.scalars().first() is not None:
|
||||||
existing.updated_at = datetime.now(UTC)
|
continue
|
||||||
existing.status = DocumentStatus.pending()
|
|
||||||
documents.append(existing)
|
|
||||||
continue
|
|
||||||
|
|
||||||
duplicate = await self.session.execute(
|
document = Document(
|
||||||
select(Document).filter(Document.content_hash == content_hash)
|
title=connector_doc.title,
|
||||||
)
|
document_type=connector_doc.document_type,
|
||||||
if duplicate.scalars().first() is not None:
|
content="Pending...",
|
||||||
continue
|
content_hash=content_hash,
|
||||||
|
unique_identifier_hash=unique_identifier_hash,
|
||||||
|
source_markdown=connector_doc.source_markdown,
|
||||||
|
document_metadata=connector_doc.metadata,
|
||||||
|
search_space_id=connector_doc.search_space_id,
|
||||||
|
connector_id=connector_doc.connector_id,
|
||||||
|
created_by_id=connector_doc.created_by_id,
|
||||||
|
updated_at=datetime.now(UTC),
|
||||||
|
status=DocumentStatus.pending(),
|
||||||
|
)
|
||||||
|
self.session.add(document)
|
||||||
|
documents.append(document)
|
||||||
|
|
||||||
document = Document(
|
await self.session.commit()
|
||||||
title=connector_doc.title,
|
return documents
|
||||||
document_type=connector_doc.document_type,
|
|
||||||
content="Pending...",
|
|
||||||
content_hash=content_hash,
|
|
||||||
unique_identifier_hash=unique_identifier_hash,
|
|
||||||
source_markdown=connector_doc.source_markdown,
|
|
||||||
document_metadata=connector_doc.metadata,
|
|
||||||
search_space_id=connector_doc.search_space_id,
|
|
||||||
connector_id=connector_doc.connector_id,
|
|
||||||
created_by_id=connector_doc.created_by_id,
|
|
||||||
updated_at=datetime.now(UTC),
|
|
||||||
status=DocumentStatus.pending(),
|
|
||||||
)
|
|
||||||
self.session.add(document)
|
|
||||||
documents.append(document)
|
|
||||||
|
|
||||||
await self.session.commit()
|
except IntegrityError:
|
||||||
return documents
|
# Most likely a concurrent worker committed a document with the same
|
||||||
|
# content_hash or unique_identifier_hash. Roll back and let the next
|
||||||
|
# sync run handle it.
|
||||||
|
await self.session.rollback()
|
||||||
|
return []
|
||||||
|
|
||||||
async def index(
|
async def index(
|
||||||
self, document: Document, connector_doc: ConnectorDocument, llm
|
self, document: Document, connector_doc: ConnectorDocument, llm
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,31 @@ async def test_updated_at_advances_when_content_changes(
|
||||||
assert updated_at_v2 > updated_at_v1
|
assert updated_at_v2 > updated_at_v1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_same_content_from_different_source_skipped_in_single_batch(
|
||||||
|
db_session, db_search_space, make_connector_document
|
||||||
|
):
|
||||||
|
first = make_connector_document(
|
||||||
|
search_space_id=db_search_space.id,
|
||||||
|
unique_id="source-a",
|
||||||
|
source_markdown="## Shared content",
|
||||||
|
)
|
||||||
|
second = make_connector_document(
|
||||||
|
search_space_id=db_search_space.id,
|
||||||
|
unique_id="source-b",
|
||||||
|
source_markdown="## Shared content",
|
||||||
|
)
|
||||||
|
service = IndexingPipelineService(session=db_session)
|
||||||
|
|
||||||
|
results = await service.prepare_for_indexing([first, second])
|
||||||
|
|
||||||
|
assert len(results) == 1
|
||||||
|
|
||||||
|
result = await db_session.execute(
|
||||||
|
select(Document).filter(Document.search_space_id == db_search_space.id)
|
||||||
|
)
|
||||||
|
assert len(result.scalars().all()) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_same_content_from_different_source_is_skipped(
|
async def test_same_content_from_different_source_is_skipped(
|
||||||
db_session, db_search_space, make_connector_document
|
db_session, db_search_space, make_connector_document
|
||||||
):
|
):
|
||||||
|
|
@ -288,3 +313,5 @@ async def test_title_and_content_change_updates_both_and_returns_document(
|
||||||
|
|
||||||
assert reloaded.title == "Updated Title"
|
assert reloaded.title == "Updated Title"
|
||||||
assert reloaded.source_markdown == "## v2"
|
assert reloaded.source_markdown == "## v2"
|
||||||
|
|
||||||
|
# explain how this No no_autoflush guard for duplicate check is a regression in new pipeline , explain this Notion chunks wrong string Behavioral diff Chunks page content Would chunk full wrapper , let us discuss about this : GitHub can't split embedding vs chunk content Behavioral diff Two strings One source_markdown
|
||||||
Loading…
Add table
Add a link
Reference in a new issue