Skip to content

Embeddings

Every name on this page is importable from sci_etl_core.embeddings.

sci_etl_core.embeddings.async_base

AsyncEmbedder

Bases: ABC

Turn text into dense vectors so records can be matched by meaning.

A single call embeds a batch of texts and returns one vector per input, in the same order. Vectors are plain list[float] to keep the contract free of any numeric-library dependency.

usage property

usage: TokenUsage | None

Tokens this embedder has used so far, or None when it does not track usage.

embed abstractmethod async

embed(texts: Sequence[str]) -> list[list[float]]

Return one embedding vector per input text, preserving order.

sci_etl_core.embeddings.openai_compatible_async

AsyncOpenAIEmbedder

AsyncOpenAIEmbedder(
    api_key: str | SecretStr,
    base_url: str,
    model: str,
    batch_size: int = 128,
    max_retries: int = 3,
    backoff_factor: float = 2.0,
    sleep: Any = sleep,
    max_retry_after: float = 60.0,
    rate_limiter: RateLimiting | None = None,
)

Bases: AsyncEmbedder

Embed text through any OpenAI-compatible /embeddings endpoint.

Configure the embedder.

This embedder is the only retry layer: the OpenAI SDK's own retries are turned off, so max_retries is the total number of attempts per batch. Between attempts it waits backoff_factor ** attempt seconds, or longer when the server's retry-after-ms or Retry-After header asks for it, up to max_retry_after seconds.

Every attempt first enters rate_limiter, an :class:~sci_etl_core.rate_limiter.AsyncRateLimiter or a :class:~sci_etl_core.rate_limiter.HostRateLimiter matched against base_url, and releases it once the response arrives. Share one limiter between a chat client and an embedder that call the same provider to keep both inside one budget.

Raises:

Type Description
ValueError

max_retries is less than 1, which would fail every request without making a single attempt, or max_retry_after is negative.

usage property

usage: TokenUsage

Tokens reported across every response received so far, as a snapshot.

embed async

embed(texts: Sequence[str]) -> list[list[float]]

aclose async

aclose() -> None

sci_etl_core.embeddings.sentence_transformer_async

AsyncSentenceTransformerEmbedder

AsyncSentenceTransformerEmbedder(
    model_name: str = "all-MiniLM-L6-v2", model: Any = None
)

Bases: AsyncEmbedder

Embed text locally with the optional sentence-transformers package.

Runs with no network access, trading a heavier install for zero per-request cost. The blocking encode call is offloaded to a worker thread so the event loop is never stalled. A preloaded model may be injected to keep the hard dependency out of import-time and test paths.

embed async

embed(texts: Sequence[str]) -> list[list[float]]

Return one vector per text.

Raises:

Type Description
EmbeddingError

The model failed to encode the batch, for example by running out of memory.

sci_etl_core.embeddings.chunking

TextChunker

Bases: ABC

Split a document body into passages small enough to embed individually.

chunk abstractmethod

chunk(text: str) -> list[str]

Return an ordered list of passages covering text.

SlidingWindowChunker

SlidingWindowChunker(
    chunk_words: int = 350, overlap_words: int | None = None
)

Bases: TextChunker

Fixed-size word windows with overlap between neighbors.

Word counts approximate token counts (English runs ~1.3 tokens per word), so the default keeps each window comfortably under the 512-token ceiling of the common small embedding models. The overlap preserves context that would otherwise be severed at a window boundary.

When overlap_words is left unset it is derived from chunk_words so a small custom window never collides with the standard overlap; an explicit overlap is always validated strictly.

chunk_words property

chunk_words: int

How many words a window holds.

overlap_words property

overlap_words: int

How many words each window repeats from the one before it.

chunk

chunk(text: str) -> list[str]

sci_etl_core.embeddings.store_base

EmbeddingChunk dataclass

EmbeddingChunk(
    record_id: str,
    chunk_index: int,
    text: str,
    vector: list[float],
    metadata: dict[str, Any] = dict(),
)

A single passage of an article, paired with its embedding vector.

record_id instance-attribute

record_id: str

chunk_index instance-attribute

chunk_index: int

text instance-attribute

text: str

vector instance-attribute

vector: list[float]

metadata class-attribute instance-attribute

metadata: dict[str, Any] = field(default_factory=dict)

SearchHit dataclass

SearchHit(
    record_id: str,
    chunk_index: int,
    text: str,
    score: float,
    metadata: dict[str, Any] = dict(),
)

A stored chunk returned from a similarity query, with its score.

record_id instance-attribute

record_id: str

chunk_index instance-attribute

chunk_index: int

text instance-attribute

text: str

score instance-attribute

score: float

metadata class-attribute instance-attribute

metadata: dict[str, Any] = field(default_factory=dict)

StoredRecord dataclass

StoredRecord(
    record_id: str,
    passages: tuple[str, ...],
    metadata: dict[str, Any] = dict(),
)

The passages a vector memory holds for one record, without their vectors.

passages are the chunk texts in chunk_index order, and metadata is the metadata of the record's first chunk, which for chunks stored by :class:~sci_etl_core.embeddings.ingest_async.AsyncChunkIngestor holds the record's title and source_url.

record_id instance-attribute

record_id: str

passages instance-attribute

passages: tuple[str, ...]

metadata class-attribute instance-attribute

metadata: dict[str, Any] = field(default_factory=dict)

AsyncEmbeddingStore

Bases: ABC

An accumulating vector memory of article chunks, searchable by meaning.

add abstractmethod async

add(chunks: Sequence[EmbeddingChunk]) -> None

Persist chunk embeddings, replacing any with the same id and index.

delete_record abstractmethod async

delete_record(record_id: str) -> None

Remove every stored chunk belonging to record_id.

replace_record async

replace_record(
    record_id: str, chunks: Sequence[EmbeddingChunk]
) -> None

Make chunks the only stored chunks of record_id.

Adding alone would leave a record's old higher-index chunks behind when its text now yields fewer passages. The default deletes and then adds; a backend that can do both atomically should override it.

query abstractmethod async

query(
    vector: Sequence[float],
    top_k: int = 5,
    min_score: float = -1.0,
    exclude_record_id: str | None = None,
) -> list[SearchHit]

Return the nearest stored chunks to vector by cosine similarity.

exclude_record_id drops a record's own chunks, so an article can be compared against every other article without matching itself. A stored vector whose score is not finite, such as one holding NaN, never matches.

count abstractmethod async

count() -> int

Return the number of stored chunks.

iter_records

iter_records(
    batch_size: int = 100,
) -> AsyncIterator[StoredRecord]

Yield every stored record's passages, in record_id order, without loading vectors.

batch_size is how many records are read at a time. A backend that cannot enumerate its chunks keeps this default, which raises; the bundled stores implement it.

Raises:

Type Description
NotImplementedError

The store cannot enumerate its records.

aclose async

aclose() -> None

Release any held resources. No-op by default.

sci_etl_core.embeddings.store_memory

InMemoryEmbeddingStore

InMemoryEmbeddingStore()

Bases: AsyncEmbeddingStore

Non-persistent store backed by a dict. Vectors are unit-normalized.

Chunks are keyed by (record_id, chunk_index) so that re-adding an article replaces its earlier chunks instead of accumulating duplicates. That mirrors the primary key of the durable SQLite store, keeping the two backends interchangeable: re-ingesting a record must not silently give it extra weight in one of them.

add async

add(chunks: Sequence[EmbeddingChunk]) -> None

delete_record async

delete_record(record_id: str) -> None

query async

query(
    vector: Sequence[float],
    top_k: int = 5,
    min_score: float = -1.0,
    exclude_record_id: str | None = None,
) -> list[SearchHit]

count async

count() -> int

iter_records async

iter_records(
    batch_size: int = 100,
) -> AsyncIterator[StoredRecord]

Yield every stored record's passages, from a snapshot taken when iteration starts.

Raises:

Type Description
ValueError

batch_size is less than 1.

sci_etl_core.embeddings.store_sqlite_async

AsyncSqliteEmbeddingStore

AsyncSqliteEmbeddingStore(path: str | Path)

Bases: AsyncEmbeddingStore

Durable vector memory persisting unit-normalized float32 chunk vectors.

Backed by the standard-library sqlite3 driver run off the event loop, so it carries no third-party dependency. The single connection must never be used by two worker threads at once, so every operation holds an :class:asyncio.Lock until its thread finishes, even if the awaiting task is cancelled meanwhile. Each write is one transaction, rolled back on failure, and every SQLite failure (including a file that is not a database) surfaces as :class:EmbeddingStoreError, as does a file written by a newer sci-etl-core. The file records its schema version in PRAGMA user_version.

Similarity is a linear scan computed in NumPy: every stored vector is loaded and scored against the query in one matrix product, and only the best chunks' text and metadata are read. The decoded vectors stay in memory between queries and are reloaded only after a write, from this store or any other connection to the file, so repeated queries skip the read. This is exact and dependency-light, and fits corpora up to the low millions of chunks; swap in an ANN index behind this same interface if the memory outgrows a full scan.

The file is opened, and its schema created, on first use. Use each instance from one event loop.

add async

add(chunks: Sequence[EmbeddingChunk]) -> None

delete_record async

delete_record(record_id: str) -> None

replace_record async

replace_record(
    record_id: str, chunks: Sequence[EmbeddingChunk]
) -> None

Delete the record's chunks and insert chunks in one transaction.

query async

query(
    vector: Sequence[float],
    top_k: int = 5,
    min_score: float = -1.0,
    exclude_record_id: str | None = None,
) -> list[SearchHit]

count async

count() -> int

iter_records async

iter_records(
    batch_size: int = 100,
) -> AsyncIterator[StoredRecord]

Yield every stored record's passages, reading batch_size records at a time.

Each batch is read in one transaction and continues after the last record_id of the batch before, so records written during iteration are yielded when their id sorts after that point, and a record never repeats. No vector is loaded.

Raises:

Type Description
ValueError

batch_size is less than 1.

EmbeddingStoreError

The store cannot be read, or a chunk's metadata is not a JSON object.

aclose async

aclose() -> None

Close the connection; a later call transparently reopens it.

sci_etl_core.embeddings.ingest_async

AsyncChunkIngestor

AsyncChunkIngestor(
    chunker: TextChunker,
    embedder: AsyncEmbedder,
    store: AsyncEmbeddingStore,
)

Chunk an article's full text, embed each passage, and add it to memory.

Intended as the second stage after an abstract-level relevance gate: only articles that already cleared the gate reach here, so the cost of embedding a whole body is spent only on records worth remembering. It satisfies :class:~sci_etl_core.ingest_protocol.MemoryIngestor.

ingest async

ingest(record: RawRecord, text: str) -> int

Replace the record's stored passages with those of text.

Every earlier chunk of the record is removed, so re-ingesting text that yields fewer passages leaves no stale chunks behind, and text with no passages clears the record from memory. Each chunk's metadata is the record's title and source_url only; record.metadata is not stored in the vector memory.

Returns:

Type Description
int

The number of chunks stored.

Raises:

Type Description
EmbeddingError

The embedder returned a different number of vectors than it was given passages.

EmbeddingStoreError

The store could not be written.

sci_etl_core.embeddings.finder_async

AsyncSimilarArticleFinder

AsyncSimilarArticleFinder(
    embedder: AsyncEmbedder, store: AsyncEmbeddingStore
)

Search the accumulated memory for passages resembling a piece of text.

find_similar_chunks async

find_similar_chunks(
    text: str,
    top_k: int = 5,
    min_score: float = 0.0,
    exclude_record_id: str | None = None,
) -> list[SearchHit]

Return up to top_k stored chunks most similar to text, best first.

text is embedded in one call, and the other arguments are those of :meth:~sci_etl_core.embeddings.store_base.AsyncEmbeddingStore.query, except that min_score defaults to 0. An embedder that returns no vector gives no hits.

find_similar_articles async

find_similar_articles(
    text: str,
    top_k: int = 5,
    min_score: float = 0.0,
    exclude_record_id: str | None = None,
    chunk_pool: int = 50,
) -> list[tuple[str, float, dict[str, Any]]]

Rank whole articles, scoring each by its single best-matching chunk.

The chunk_pool best chunks are collapsed to one entry per record, so at most chunk_pool records come back, and often far fewer when long articles own many of those chunks; raise chunk_pool along with top_k. Each entry is (record_id, score, metadata), where metadata is the best chunk's; :meth:find_best_chunks also returns the chunk's text.

find_best_chunks async

find_best_chunks(
    text: str,
    top_k: int = 5,
    min_score: float = 0.0,
    exclude_record_id: str | None = None,
    chunk_pool: int = 50,
) -> list[SearchHit]

Rank whole articles as :meth:find_similar_articles does, returning each one's best chunk.

Each hit is the chunk that scored its record, text included, so a result can show the passage that made it similar. When two chunks of a record score the same, the one ranked first by the store is kept.