Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
435 changes: 435 additions & 0 deletions COSMOSDB_EMULATOR_TESTING.md

Large diffs are not rendered by default.

513 changes: 513 additions & 0 deletions INSTRUCTIONS.md

Large diffs are not rendered by default.

437 changes: 437 additions & 0 deletions README.md

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,20 @@ module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['<rootDir>/sample/', '<rootDir>/dist/'],
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: '.',
testMatch: ['**/tests/**/*.spec.ts', '**/lib/**/*.spec.ts'],
collectCoverageFrom: [
'lib/**/*.(t|j)s',
'!lib/**/*.spec.ts',
'!lib/**/index.ts',
],
transform: {
'^.+\\.(t|j)s$': 'ts-jest',
},
globals: {
'ts-jest': {
tsconfig: 'tests/tsconfig.json',
},
},
};
2 changes: 2 additions & 0 deletions lib/cosmos-db/__tests__/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Jest test setup file
import 'reflect-metadata';
114 changes: 113 additions & 1 deletion lib/cosmos-db/cosmos-db.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CosmosClientOptions } from '@azure/cosmos';
import { CosmosClientOptions, FeedOptions } from '@azure/cosmos';
import { Type } from '@nestjs/common';
import { ModuleMetadata } from '@nestjs/common/interfaces';

Expand All @@ -9,6 +9,66 @@ export interface AzureCosmosDbOptions extends CosmosClientOptions {
connectionName?: string;
}

/**
* Vector search configuration for similarity queries on embeddings
*/
export interface VectorSearchOptions {
/** The vector field path to search against */
vectorPath: string;
/** The query vector for similarity search */
vector: number[];
/** Number of similar vectors to return */
limit?: number;
/** Similarity threshold (0-1) */
similarityThreshold?: number;
/** Distance function to use for vector similarity */
distanceFunction?: 'cosine' | 'dotproduct' | 'euclidean';
}

/**
* Full-text search configuration for keyword and text matching
*/
export interface FullTextSearchOptions {
/** The text to search for */
searchText: string;
/** Fields to search in. If not specified, searches all text fields */
searchFields?: string[];
/** Search mode: 'any' matches any term, 'all' requires all terms */
searchMode?: 'any' | 'all';
/** Enable fuzzy matching for typos */
fuzzySearch?: boolean;
/** Highlight matched terms in results */
highlightFields?: string[];
}

/**
* Hybrid search configuration combining vector and text search
*/
export interface HybridSearchOptions {
/** Vector search configuration */
vectorSearch: VectorSearchOptions;
/** Full-text search configuration */
fullTextSearch: FullTextSearchOptions;
/** Weight for vector search results (0-1, default 0.5) */
vectorWeight?: number;
/** Weight for text search results (0-1, default 0.5) */
textWeight?: number;
/** Ranking function for combining results */
rankingFunction?: 'rrf' | 'weighted' | 'linear';
}

/**
* Extended feed options that include search capabilities
*/
export interface ExtendedFeedOptions extends FeedOptions {
/** Enable vector search buffer optimization */
vectorSearchBufferSize?: number;
/** Allow unbounded vector search queries */
allowUnboundedVectorQueries?: boolean;
/** Disable hybrid search query plan optimization */
disableHybridSearchQueryPlanOptimization?: boolean;
}

export interface AzureCosmosDbOptionsFactory {
createAzureCosmosDbOptions(): Promise<AzureCosmosDbOptions> | AzureCosmosDbOptions;
}
Expand All @@ -21,6 +81,58 @@ export interface AzureCosmosDbModuleAsyncOptions extends Pick<ModuleMetadata, 'i
inject?: any[];
}

/**
* Search result with relevance scoring
*/
export interface SearchResult<T = any> {
/** The document data */
document: T;
/** Relevance score (0-1) */
score: number;
/** Rank position in results */
rank?: number;
/** Search highlights for matched terms */
highlights?: Record<string, string[]>;
}

/**
* Vector search result with similarity scoring
*/
export interface VectorSearchResult<T = any> extends SearchResult<T> {
/** Vector similarity score */
similarityScore: number;
/** Distance from query vector */
distance?: number;
}

/**
* Full-text search result with text relevance
*/
export interface FullTextSearchResult<T = any> extends SearchResult<T> {
/** Text relevance score */
textScore: number;
/** Matched terms */
matchedTerms?: string[];
}

/**
* Hybrid search result combining vector and text scores
*/
export interface HybridSearchResult<T = any> extends SearchResult<T> {
/** Combined relevance score */
combinedScore: number;
/** Vector similarity score */
vectorScore: number;
/** Text relevance score */
textScore: number;
/** Ranking details */
rankingDetails?: {
vectorRank: number;
textRank: number;
fusionScore: number;
};
}

type GeoJsonTypes = 'Point' | 'Polygon' | 'LineStrings';

export type Position = number[]; // [number, number] | [number, number, number]; Longitude, Latitude
Expand Down
9 changes: 7 additions & 2 deletions lib/cosmos-db/cosmos-db.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,34 @@ import { DynamicModule, Module } from '@nestjs/common';
import { AzureCosmosDbCoreModule } from './cosmos-db-core.module';
import { AzureCosmosDbModuleAsyncOptions, AzureCosmosDbOptions } from './cosmos-db.interface';
import { createAzureCosmosDbProviders } from './cosmos-db.providers';
import { CosmosSearchService } from './cosmos-search.service';

@Module({})
export class AzureCosmosDbModule {
static forRoot(options: AzureCosmosDbOptions): DynamicModule {
return {
module: AzureCosmosDbModule,
imports: [AzureCosmosDbCoreModule.forRoot(options)],
providers: [CosmosSearchService],
exports: [CosmosSearchService],
};
}

static forRootAsync(options: AzureCosmosDbModuleAsyncOptions): DynamicModule {
return {
module: AzureCosmosDbModule,
imports: [AzureCosmosDbCoreModule.forRootAsync(options)],
providers: [CosmosSearchService],
exports: [CosmosSearchService],
};
}

static forFeature(models: { dto: any; collection?: string }[] = [], connectionName?: string): DynamicModule {
const providers = createAzureCosmosDbProviders(connectionName, models);
return {
module: AzureCosmosDbModule,
providers,
exports: providers,
providers: [...providers, CosmosSearchService],
exports: [...providers, CosmosSearchService],
};
}
}
156 changes: 156 additions & 0 deletions lib/cosmos-db/cosmos-search.decorators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import 'reflect-metadata';

/**
* Metadata key for vector search configuration
*/
export const VECTOR_SEARCH_METADATA_KEY = 'vectorSearch';

/**
* Metadata key for full-text search configuration
*/
export const FULLTEXT_SEARCH_METADATA_KEY = 'fullTextSearch';

/**
* Configuration for vector embeddings on a property
*/
export interface VectorEmbeddingConfig {
/** Data type of the vector elements */
dataType?: 'float32' | 'uint8' | 'int8';
/** Number of dimensions in the vector */
dimensions: number;
/** Distance function for similarity calculations */
distanceFunction?: 'cosine' | 'dotproduct' | 'euclidean';
/** Vector index type for optimization */
indexType?: 'flat' | 'quantizedFlat' | 'diskANN';
}

/**
* Configuration for full-text search on a property
*/
export interface FullTextConfig {
/** Whether this field should be included in full-text search */
searchable?: boolean;
/** Whether this field should be highlighted in search results */
highlightable?: boolean;
/** Analyzer to use for text processing */
analyzer?: 'standard' | 'keyword' | 'simple';
/** Weight of this field in text relevance scoring */
weight?: number;
}

/**
* Decorator to mark a property as a vector embedding for similarity search
*
* @example
* ```typescript
* export class Article {
* @VectorEmbedding({
* dimensions: 1536,
* distanceFunction: 'cosine',
* indexType: 'flat'
* })
* embedding: number[];
* }
* ```
*/
export function VectorEmbedding(config: VectorEmbeddingConfig): PropertyDecorator {
return (target: any, propertyKey: string | symbol) => {
const existingMetadata = Reflect.getMetadata(VECTOR_SEARCH_METADATA_KEY, target) || {};
existingMetadata[propertyKey] = config;
Reflect.defineMetadata(VECTOR_SEARCH_METADATA_KEY, existingMetadata, target);
};
}

/**
* Decorator to mark a property for full-text search capabilities
*
* @example
* ```typescript
* export class Article {
* @FullTextSearchable({
* searchable: true,
* highlightable: true,
* weight: 2.0
* })
* title: string;
*
* @FullTextSearchable({
* searchable: true,
* highlightable: false,
* weight: 1.0
* })
* content: string;
* }
* ```
*/
export function FullTextSearchable(config: FullTextConfig = {}): PropertyDecorator {
return (target: any, propertyKey: string | symbol) => {
const existingMetadata = Reflect.getMetadata(FULLTEXT_SEARCH_METADATA_KEY, target) || {};
existingMetadata[propertyKey] = { searchable: true, ...config };
Reflect.defineMetadata(FULLTEXT_SEARCH_METADATA_KEY, existingMetadata, target);
};
}

/**
* Get vector embedding metadata for a class
*/
export function getVectorEmbeddingMetadata(target: any): Record<string, VectorEmbeddingConfig> {
return Reflect.getMetadata(VECTOR_SEARCH_METADATA_KEY, target) || {};
}

/**
* Get full-text search metadata for a class
*/
export function getFullTextSearchMetadata(target: any): Record<string, FullTextConfig> {
return Reflect.getMetadata(FULLTEXT_SEARCH_METADATA_KEY, target) || {};
}

/**
* Helper to get searchable field names from a class
*/
export function getSearchableFields(target: any): string[] {
const metadata = getFullTextSearchMetadata(target);
return Object.entries(metadata)
.filter(([, config]) => config.searchable)
.map(([fieldName]) => fieldName);
}

/**
* Helper to get highlightable field names from a class
*/
export function getHighlightableFields(target: any): string[] {
const metadata = getFullTextSearchMetadata(target);
return Object.entries(metadata)
.filter(([, config]) => config.highlightable)
.map(([fieldName]) => fieldName);
}

/**
* Helper to get vector field names from a class
*/
export function getVectorFields(target: any): string[] {
const metadata = getVectorEmbeddingMetadata(target);
return Object.keys(metadata);
}

/**
* Get vector embedding configuration for a specific property
*/
export function getVectorEmbeddingConfig(
target: any,
propertyKey: string | symbol,
): VectorEmbeddingConfig | undefined {
const metadata = getVectorEmbeddingMetadata(target);
return metadata[propertyKey as string];
}

/**
* Get full-text search configuration for a specific property
*/
export function getFullTextConfig(
target: any,
propertyKey: string | symbol,
): FullTextConfig | undefined {
const metadata = getFullTextSearchMetadata(target);
return metadata[propertyKey as string];
}
Loading