Embeddings Creation & Retrieval
Overview of Creating and Searching Embeddings¶
Dual Embedding Strategy¶
OpenContracts uses a dual embedding strategy to enable both global cross-corpus search and corpus-optimized search:
- Default Embedder Embedding (Always Created)
- Every annotation, document, and note ALWAYS gets an embedding using the platform's default embedder (configured via
PipelineSettings.default_embedderin the database) - This creates a common vector space for global search across all corpuses
-
Enables users to search all their documents regardless of which corpus they belong to
-
Corpus-Specific Embedding (Created When Different)
- If a corpus has a
preferred_embedderthat differs from the default embedder, annotations also get an embedding using the corpus embedder - Enables optimized search within a corpus using domain-specific models (e.g., legal-bert for legal documents)
Example Scenarios:
| Corpus Embedder | Embeddings Created |
|---|---|
| None (default) | 1x default embedder |
| Same as default embedder | 1x default embedder |
| Different embedder | 2x (default embedder + corpus embedder) |
This strategy is implemented in the embedding tasks (opencontractserver/tasks/embeddings_task.py) which automatically handle the dual embedding creation.
Creating Embeddings¶
- Generate the Embeddings (Text → Vector)
- Use the unified utility function
generate_embeddings_from_text(text, embedder_path=None)oragenerate_embeddings_from_text(text, embedder_path=None)fromopencontractserver/utils/embeddings.py. -
These functions:
- Retrieve any configured Python embedder class for the specified corpus (if
corpus_idis provided toget_embedder()). - If no embedder is found or it fails, fall back to the configured default embedder (typically our built-in microservice).
- Return a tuple
(embedder_path, vector), allowing you to know which embedder was used and the numeric embedding vector.
- Retrieve any configured Python embedder class for the specified corpus (if
-
Store the Embeddings
- Suppose you have a model instance (e.g.,
Annotation,Document,Note) that uses theHasEmbeddingMixin. - Once you have
(embedder_path, vector)from the generation step, you call:instance.add_embedding(embedder_path="my-embedder", vector=[...]) - Internally, this uses:
- The
HasEmbeddingMixin.add_embedding(...)method, which contacts theEmbeddingmanager to create or update an embedding record associated with this instance. - For multiple vectors (e.g., one instance with many embeddings), use:
instance.add_embeddings("my-embedder", [...multiple_vectors...])
- The
-
This is the "write" step—saving the vectors to your database.
-
Retrieve Stored Embeddings
- To retrieve a stored embedding vector:
# Synchronous retrieval vector = instance.get_embedding( embedder_path="openai/text-embedding-ada-002", dimension=384 ) # Asynchronous retrieval (uses database_sync_to_async) vector = await instance.aget_embedding( embedder_path="openai/text-embedding-ada-002", dimension=384 ) - Returns
List[float] | None- the embedding vector or None if not found.
Embedding Storage Architecture¶
The embedding system uses a dedicated Embedding model that supports multiple vector dimensions:
- Supported Dimensions: 384, 768, 1024, 1536, 2048, 3072, 4096
- Vector Fields:
vector_384,vector_768,vector_1024,vector_1536,vector_2048,vector_3072,vector_4096 - Reference Fields:
document_id,annotation_id,note_id(depending on the model type) - Embedder Tracking:
embedder_pathfield stores the identifier of the embedding model used
Embedder Configuration¶
Embedder settings are managed via the PipelineSettings database singleton (see opencontractserver/documents/models.py):
PipelineSettings.preferred_embedders: Map of MIME types to preferred embedder class pathsPipelineSettings.default_embedder: Fallback embedder when no preferred embedder is foundDEFAULT_EMBEDDING_DIMENSION: Default dimension (768, inconfig/settings/base.py)
These values are seeded from Django settings on first creation but are managed at runtime via the admin UI or GraphQL mutations. Changes take effect immediately across all processes (cached via Django's cache framework with a 5-minute TTL).
Available embedder implementations in opencontractserver/pipeline/embedders/:
Text-only: - MicroserviceEmbedder: Calls external sentence-transformer microservice (384-dim, default)
Multimodal (text + images): - CLIPMicroserviceEmbedder: CLIP-based multimodal embedder via microservice (768-dim) - QwenMicroserviceEmbedder: Qwen-based multimodal embedder via microservice (1024-dim)
Searching Embeddings¶
Our search architecture is designed with two layers: a core API that contains our business logic, and framework adapters that provide compatibility with different agent frameworks.
Core Search API¶
CoreAnnotationVectorStore- Framework-Agnostic Business Logic- Located in
opencontractserver/llms/vector_stores/core_vector_stores.py - Contains all the business logic for vector search without dependencies on specific agent frameworks
- Key classes:
CoreAnnotationVectorStore,VectorSearchQuery,VectorSearchResult -
See the source file for initialization and usage examples
-
Framework-Agnostic Data Structures
VectorSearchQuery: Contains query text/embedding, filters, and search parameters-
VectorSearchResult: Contains the annotation and similarity score -
Filtering Logic
- Corpus Filtering: Annotations are filtered by corpus membership. Structural annotations (
structural=True) are always included regardless of corpus, while non-structural annotations must belong to the specified corpus. - Document Filtering: When
document_idis provided, only annotations from that document are included. - Visibility Filtering: Annotations are visible if they are structural, public, or created by the requesting user.
- Metadata Filtering: Additional filters can be applied on annotation labels and other fields.
Global Search¶
The CoreAnnotationVectorStore provides a global_search class method for cross-corpus search:
from opencontractserver.llms.vector_stores.core_vector_stores import (
CoreAnnotationVectorStore,
VectorSearchResult,
)
# Search across ALL documents the user has access to
results = CoreAnnotationVectorStore.global_search(
user_id=my_user_id,
query_text="contract termination clause",
top_k=100,
modalities=["TEXT"], # Optional: filter by content type
)
# Async version
results = await CoreAnnotationVectorStore.async_global_search(
user_id=my_user_id,
query_text="contract termination clause",
top_k=100,
)
# Process results
for result in results:
annotation = result.annotation
similarity = result.similarity_score
document_title = annotation.document.title if annotation.document else "N/A"
print(f"Found: {annotation.raw_text[:50]}... (score: {similarity:.3f})")
Key Features: - Uses default embedder embeddings for consistent cross-corpus search - Respects user permissions (only searches documents the user can access) - Supports modality filtering (TEXT, IMAGE, etc.) - Works with both standalone and corpus-bound documents
Vector Search Mixin¶
Models that store embeddings can use VectorSearchViaEmbeddingMixin from opencontractserver/shared/mixins.py to enable vector similarity searches (the model must also use HasEmbeddingMixin). See the source file for usage examples.
Framework Adapters¶
Framework adapters are thin wrappers that translate between the core API and specific agent frameworks:
- LlamaIndex Adapter - Removed
- The LlamaIndex vector store adapter has been removed from the codebase.
-
If you need LlamaIndex integration, implement your own adapter wrapping
CoreAnnotationVectorStore. -
PydanticAI Adapter -
PydanticAIAnnotationVectorStore - Located in
opencontractserver/llms/vector_stores/pydantic_ai_vector_stores.py - Provides async-first API with Pydantic models for type safety
-
Converts between PydanticAI types and our core types (see source file for usage examples)
-
PydanticAI Tool Creation
-
Use
create_vector_search_tool()frompydantic_ai_vector_stores.pyto create vector search tools for PydanticAI agents. -
PydanticAI Response Format
- Returns structured
PydanticAIVectorSearchResponsewith validated data containing annotation details, similarity scores, and location information. - See
pydantic_ai_vector_stores.pyfor the complete response structure.
Async/Sync Compatibility¶
The embedding system supports both synchronous and asynchronous operations:
Synchronous Methods: - generate_embeddings_from_text() - instance.get_embedding() - instance.add_embedding() - store.search()
Asynchronous Methods: - agenerate_embeddings_from_text() - instance.aget_embedding() (uses database_sync_to_async) - store.async_search()
When working in async contexts (such as Django Channels WebSocket consumers), always use the async variants to avoid blocking operations.
How This Works in Practice¶
- Query Processing
- Framework adapter receives a query in framework-specific format
- Adapter converts to
VectorSearchQuery -
Core store processes the query using our business logic
-
Vector Generation
- If
query.query_textis provided, core store callsgenerate_embeddings_from_text(...)oragenerate_embeddings_from_text(...) -
If
query.query_embeddingis provided, uses it directly -
Database Search
- Core store builds Django queryset with filters (corpus, document, user, metadata)
- Applies structural annotation logic (structural annotations bypass corpus filtering)
- Uses
.search_by_embedding(...)fromVectorSearchViaEmbeddingMixinfor similarity search -
Returns
VectorSearchResultobjects with annotations and similarity scores -
Result Conversion
- Framework adapter converts
VectorSearchResultobjects to framework-specific format - Embedding vectors are retrieved using
aget_embedding()for async contexts
Performance Considerations¶
-
Database Indexes: The
Annotationmodel includes composite indexes for optimal query performance. Seeopencontractserver/annotations/models.pyfor index definitions. -
Embedding Retrieval: Use
prefetch_related("embeddings")when fetching multiple annotations to avoid N+1 queries. -
Vector Dimensions: Choose appropriate embedding dimensions based on your use case:
- 384: Fast, good for general similarity
- 768: Balanced performance and accuracy
- 1536: High accuracy, more computational cost
- 3072: Highest accuracy, highest computational cost
Architecture Benefits¶
This architecture provides: - Reusability: Core logic works with any framework - Maintainability: Business logic changes in one place - Extensibility: Easy to add new framework adapters - Type Safety: Clear interfaces between layers - Testing: Core functionality can be tested independently - Async Support: Full compatibility with async Django applications - Performance: Optimized database queries and indexing strategies
By following this pattern, you can use the same underlying search capabilities across different agent frameworks while maintaining consistency and avoiding code duplication.