BREAKING CHANGE(core): Refactor to v3: introduce modular core/domain architecture, plugin system, observability and strict TypeScript configuration; remove legacy classes
This commit is contained in:
324
ts/domain/query/aggregation-builder.ts
Normal file
324
ts/domain/query/aggregation-builder.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
import type {
|
||||
AggregationDSL,
|
||||
TermsAggregation,
|
||||
MetricAggregation,
|
||||
StatsAggregation,
|
||||
ExtendedStatsAggregation,
|
||||
PercentilesAggregation,
|
||||
DateHistogramAggregation,
|
||||
HistogramAggregation,
|
||||
RangeAggregation,
|
||||
FilterAggregation,
|
||||
TopHitsAggregation,
|
||||
QueryDSL,
|
||||
SortOrder,
|
||||
SortField,
|
||||
} from './types.js';
|
||||
|
||||
/**
|
||||
* Fluent aggregation builder for type-safe Elasticsearch aggregations
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const query = new QueryBuilder<Product>('products')
|
||||
* .aggregations((agg) => {
|
||||
* agg.terms('categories', 'category.keyword', { size: 10 })
|
||||
* .subAggregation('avg_price', (sub) => sub.avg('price'));
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class AggregationBuilder {
|
||||
private aggregations: Record<string, AggregationDSL> = {};
|
||||
private currentAggName?: string;
|
||||
|
||||
/**
|
||||
* Add a terms aggregation
|
||||
*/
|
||||
terms(
|
||||
name: string,
|
||||
field: string,
|
||||
options?: {
|
||||
size?: number;
|
||||
order?: Record<string, SortOrder>;
|
||||
missing?: string | number;
|
||||
}
|
||||
): this {
|
||||
const termsAgg: TermsAggregation = {
|
||||
terms: {
|
||||
field,
|
||||
...options,
|
||||
},
|
||||
};
|
||||
this.aggregations[name] = termsAgg;
|
||||
this.currentAggName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an average metric aggregation
|
||||
*/
|
||||
avg(name: string, field: string, missing?: number): this {
|
||||
const avgAgg: MetricAggregation = {
|
||||
avg: {
|
||||
field,
|
||||
...(missing !== undefined && { missing }),
|
||||
},
|
||||
};
|
||||
this.aggregations[name] = avgAgg;
|
||||
this.currentAggName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a sum metric aggregation
|
||||
*/
|
||||
sum(name: string, field: string, missing?: number): this {
|
||||
const sumAgg: MetricAggregation = {
|
||||
sum: {
|
||||
field,
|
||||
...(missing !== undefined && { missing }),
|
||||
},
|
||||
};
|
||||
this.aggregations[name] = sumAgg;
|
||||
this.currentAggName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a min metric aggregation
|
||||
*/
|
||||
min(name: string, field: string, missing?: number): this {
|
||||
const minAgg: MetricAggregation = {
|
||||
min: {
|
||||
field,
|
||||
...(missing !== undefined && { missing }),
|
||||
},
|
||||
};
|
||||
this.aggregations[name] = minAgg;
|
||||
this.currentAggName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a max metric aggregation
|
||||
*/
|
||||
max(name: string, field: string, missing?: number): this {
|
||||
const maxAgg: MetricAggregation = {
|
||||
max: {
|
||||
field,
|
||||
...(missing !== undefined && { missing }),
|
||||
},
|
||||
};
|
||||
this.aggregations[name] = maxAgg;
|
||||
this.currentAggName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a cardinality metric aggregation
|
||||
*/
|
||||
cardinality(name: string, field: string): this {
|
||||
const cardinalityAgg: MetricAggregation = {
|
||||
cardinality: {
|
||||
field,
|
||||
},
|
||||
};
|
||||
this.aggregations[name] = cardinalityAgg;
|
||||
this.currentAggName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a stats aggregation
|
||||
*/
|
||||
stats(name: string, field: string): this {
|
||||
const statsAgg: StatsAggregation = {
|
||||
stats: {
|
||||
field,
|
||||
},
|
||||
};
|
||||
this.aggregations[name] = statsAgg;
|
||||
this.currentAggName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an extended stats aggregation
|
||||
*/
|
||||
extendedStats(name: string, field: string): this {
|
||||
const extendedStatsAgg: ExtendedStatsAggregation = {
|
||||
extended_stats: {
|
||||
field,
|
||||
},
|
||||
};
|
||||
this.aggregations[name] = extendedStatsAgg;
|
||||
this.currentAggName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a percentiles aggregation
|
||||
*/
|
||||
percentiles(name: string, field: string, percents?: number[]): this {
|
||||
const percentilesAgg: PercentilesAggregation = {
|
||||
percentiles: {
|
||||
field,
|
||||
...(percents && { percents }),
|
||||
},
|
||||
};
|
||||
this.aggregations[name] = percentilesAgg;
|
||||
this.currentAggName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a date histogram aggregation
|
||||
*/
|
||||
dateHistogram(
|
||||
name: string,
|
||||
field: string,
|
||||
options: {
|
||||
calendar_interval?: string;
|
||||
fixed_interval?: string;
|
||||
format?: string;
|
||||
time_zone?: string;
|
||||
min_doc_count?: number;
|
||||
}
|
||||
): this {
|
||||
const dateHistogramAgg: DateHistogramAggregation = {
|
||||
date_histogram: {
|
||||
field,
|
||||
...options,
|
||||
},
|
||||
};
|
||||
this.aggregations[name] = dateHistogramAgg;
|
||||
this.currentAggName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a histogram aggregation
|
||||
*/
|
||||
histogram(
|
||||
name: string,
|
||||
field: string,
|
||||
interval: number,
|
||||
options?: {
|
||||
min_doc_count?: number;
|
||||
}
|
||||
): this {
|
||||
const histogramAgg: HistogramAggregation = {
|
||||
histogram: {
|
||||
field,
|
||||
interval,
|
||||
...options,
|
||||
},
|
||||
};
|
||||
this.aggregations[name] = histogramAgg;
|
||||
this.currentAggName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a range aggregation
|
||||
*/
|
||||
range(
|
||||
name: string,
|
||||
field: string,
|
||||
ranges: Array<{ from?: number; to?: number; key?: string }>
|
||||
): this {
|
||||
const rangeAgg: RangeAggregation = {
|
||||
range: {
|
||||
field,
|
||||
ranges,
|
||||
},
|
||||
};
|
||||
this.aggregations[name] = rangeAgg;
|
||||
this.currentAggName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a filter aggregation
|
||||
*/
|
||||
filterAgg(name: string, filter: QueryDSL): this {
|
||||
const filterAgg: FilterAggregation = {
|
||||
filter,
|
||||
};
|
||||
this.aggregations[name] = filterAgg;
|
||||
this.currentAggName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a top hits aggregation
|
||||
*/
|
||||
topHits(
|
||||
name: string,
|
||||
options?: {
|
||||
size?: number;
|
||||
sort?: Array<SortField | string>;
|
||||
_source?: boolean | { includes?: string[]; excludes?: string[] };
|
||||
}
|
||||
): this {
|
||||
const topHitsAgg: TopHitsAggregation = {
|
||||
top_hits: {
|
||||
...options,
|
||||
},
|
||||
};
|
||||
this.aggregations[name] = topHitsAgg;
|
||||
this.currentAggName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a sub-aggregation to the last defined aggregation
|
||||
*/
|
||||
subAggregation(name: string, configure: (builder: AggregationBuilder) => void): this {
|
||||
if (!this.currentAggName) {
|
||||
throw new Error('Cannot add sub-aggregation: no parent aggregation defined');
|
||||
}
|
||||
|
||||
const parentAgg = this.aggregations[this.currentAggName];
|
||||
const subBuilder = new AggregationBuilder();
|
||||
configure(subBuilder);
|
||||
|
||||
// Add aggs field to parent aggregation
|
||||
if ('terms' in parentAgg) {
|
||||
(parentAgg as TermsAggregation).aggs = subBuilder.build();
|
||||
} else if ('date_histogram' in parentAgg) {
|
||||
(parentAgg as DateHistogramAggregation).aggs = subBuilder.build();
|
||||
} else if ('histogram' in parentAgg) {
|
||||
(parentAgg as HistogramAggregation).aggs = subBuilder.build();
|
||||
} else if ('range' in parentAgg) {
|
||||
(parentAgg as RangeAggregation).aggs = subBuilder.build();
|
||||
} else if ('filter' in parentAgg) {
|
||||
(parentAgg as FilterAggregation).aggs = subBuilder.build();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom aggregation DSL
|
||||
*/
|
||||
custom(name: string, aggregation: AggregationDSL): this {
|
||||
this.aggregations[name] = aggregation;
|
||||
this.currentAggName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the aggregations object
|
||||
*/
|
||||
build(): Record<string, AggregationDSL> {
|
||||
return this.aggregations;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new aggregation builder
|
||||
*/
|
||||
export function createAggregationBuilder(): AggregationBuilder {
|
||||
return new AggregationBuilder();
|
||||
}
|
||||
67
ts/domain/query/index.ts
Normal file
67
ts/domain/query/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Query Builder Module
|
||||
*
|
||||
* Type-safe query construction for Elasticsearch
|
||||
*/
|
||||
|
||||
// Query Builder
|
||||
export { QueryBuilder, createQuery } from './query-builder.js';
|
||||
|
||||
// Aggregation Builder
|
||||
export { AggregationBuilder, createAggregationBuilder } from './aggregation-builder.js';
|
||||
|
||||
// Types
|
||||
export type {
|
||||
// Query types
|
||||
QueryType,
|
||||
QueryDSL,
|
||||
BoolClause,
|
||||
BoolQuery,
|
||||
MatchQuery,
|
||||
MatchPhraseQuery,
|
||||
MultiMatchQuery,
|
||||
TermQuery,
|
||||
TermsQuery,
|
||||
RangeQuery,
|
||||
ExistsQuery,
|
||||
PrefixQuery,
|
||||
WildcardQuery,
|
||||
RegexpQuery,
|
||||
FuzzyQuery,
|
||||
IdsQuery,
|
||||
MatchAllQuery,
|
||||
QueryStringQuery,
|
||||
SimpleQueryStringQuery,
|
||||
|
||||
// Options
|
||||
SearchOptions,
|
||||
SortOrder,
|
||||
SortField,
|
||||
MatchOperator,
|
||||
MultiMatchType,
|
||||
RangeBounds,
|
||||
|
||||
// Aggregation types
|
||||
AggregationType,
|
||||
AggregationDSL,
|
||||
TermsAggregation,
|
||||
MetricAggregation,
|
||||
StatsAggregation,
|
||||
ExtendedStatsAggregation,
|
||||
PercentilesAggregation,
|
||||
DateHistogramAggregation,
|
||||
HistogramAggregation,
|
||||
RangeAggregation,
|
||||
FilterAggregation,
|
||||
TopHitsAggregation,
|
||||
|
||||
// Results
|
||||
SearchResult,
|
||||
SearchHit,
|
||||
AggregationResult,
|
||||
AggregationBucket,
|
||||
TermsAggregationResult,
|
||||
MetricAggregationResult,
|
||||
StatsAggregationResult,
|
||||
PercentilesAggregationResult,
|
||||
} from './types.js';
|
||||
629
ts/domain/query/query-builder.ts
Normal file
629
ts/domain/query/query-builder.ts
Normal file
@@ -0,0 +1,629 @@
|
||||
import type {
|
||||
QueryDSL,
|
||||
BoolQuery,
|
||||
MatchQuery,
|
||||
MatchPhraseQuery,
|
||||
MultiMatchQuery,
|
||||
TermQuery,
|
||||
TermsQuery,
|
||||
RangeQuery,
|
||||
ExistsQuery,
|
||||
PrefixQuery,
|
||||
WildcardQuery,
|
||||
RegexpQuery,
|
||||
FuzzyQuery,
|
||||
IdsQuery,
|
||||
MatchAllQuery,
|
||||
QueryStringQuery,
|
||||
SimpleQueryStringQuery,
|
||||
SearchOptions,
|
||||
SearchResult,
|
||||
SortOrder,
|
||||
MatchOperator,
|
||||
MultiMatchType,
|
||||
RangeBounds,
|
||||
SortField,
|
||||
} from './types.js';
|
||||
import type { AggregationBuilder } from './aggregation-builder.js';
|
||||
import { createAggregationBuilder } from './aggregation-builder.js';
|
||||
import { ElasticsearchConnectionManager } from '../../core/connection/connection-manager.js';
|
||||
import { defaultLogger } from '../../core/observability/logger.js';
|
||||
import { defaultMetrics } from '../../core/observability/metrics.js';
|
||||
import { defaultTracing } from '../../core/observability/tracing.js';
|
||||
|
||||
/**
|
||||
* Fluent query builder for type-safe Elasticsearch queries
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const results = await new QueryBuilder<Product>('products')
|
||||
* .match('name', 'laptop')
|
||||
* .range('price', { gte: 100, lte: 1000 })
|
||||
* .sort('price', 'asc')
|
||||
* .size(20)
|
||||
* .execute();
|
||||
* ```
|
||||
*/
|
||||
export class QueryBuilder<T = unknown> {
|
||||
private index: string;
|
||||
private queryDSL: QueryDSL | null = null;
|
||||
private boolClauses: {
|
||||
must: QueryDSL[];
|
||||
should: QueryDSL[];
|
||||
must_not: QueryDSL[];
|
||||
filter: QueryDSL[];
|
||||
} = {
|
||||
must: [],
|
||||
should: [],
|
||||
must_not: [],
|
||||
filter: [],
|
||||
};
|
||||
private minimumShouldMatch?: number | string;
|
||||
private sortFields: Array<SortField | string> = [];
|
||||
private sourceFields?: string[];
|
||||
private excludeSourceFields?: string[];
|
||||
private resultSize: number = 10;
|
||||
private resultFrom: number = 0;
|
||||
private shouldTrackTotalHits: boolean | number = true;
|
||||
private searchTimeout?: string;
|
||||
private aggregationBuilder?: AggregationBuilder;
|
||||
private highlightConfig?: SearchOptions['highlight'];
|
||||
|
||||
constructor(index: string) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new query builder instance
|
||||
*/
|
||||
static create<T>(index: string): QueryBuilder<T> {
|
||||
return new QueryBuilder<T>(index);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Query Methods
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Add a match query
|
||||
*/
|
||||
match(field: string, query: string, options?: { operator?: MatchOperator; fuzziness?: number | 'AUTO'; boost?: number }): this {
|
||||
const matchQuery: MatchQuery = {
|
||||
match: {
|
||||
[field]: {
|
||||
query,
|
||||
...options,
|
||||
},
|
||||
},
|
||||
};
|
||||
this.boolClauses.must.push(matchQuery);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a match phrase query
|
||||
*/
|
||||
matchPhrase(field: string, query: string, options?: { slop?: number; boost?: number }): this {
|
||||
const matchPhraseQuery: MatchPhraseQuery = {
|
||||
match_phrase: {
|
||||
[field]: {
|
||||
query,
|
||||
...options,
|
||||
},
|
||||
},
|
||||
};
|
||||
this.boolClauses.must.push(matchPhraseQuery);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a multi-match query
|
||||
*/
|
||||
multiMatch(query: string, fields: string[], options?: { type?: MultiMatchType; operator?: MatchOperator; boost?: number }): this {
|
||||
const multiMatchQuery: MultiMatchQuery = {
|
||||
multi_match: {
|
||||
query,
|
||||
fields,
|
||||
...options,
|
||||
},
|
||||
};
|
||||
this.boolClauses.must.push(multiMatchQuery);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a term query (exact match)
|
||||
*/
|
||||
term(field: string, value: string | number | boolean, boost?: number): this {
|
||||
const termQuery: TermQuery = {
|
||||
term: {
|
||||
[field]: {
|
||||
value,
|
||||
...(boost && { boost }),
|
||||
},
|
||||
},
|
||||
};
|
||||
this.boolClauses.filter.push(termQuery);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a terms query (match any of the values)
|
||||
*/
|
||||
terms(field: string, values: Array<string | number | boolean>, boost?: number): this {
|
||||
const termsQuery: TermsQuery = {
|
||||
terms: {
|
||||
[field]: values,
|
||||
...(boost && { boost }),
|
||||
},
|
||||
};
|
||||
this.boolClauses.filter.push(termsQuery);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a range query
|
||||
*/
|
||||
range(field: string, bounds: RangeBounds, boost?: number): this {
|
||||
const rangeQuery: RangeQuery = {
|
||||
range: {
|
||||
[field]: {
|
||||
...bounds,
|
||||
...(boost && { boost }),
|
||||
},
|
||||
},
|
||||
};
|
||||
this.boolClauses.filter.push(rangeQuery);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an exists query (field must exist)
|
||||
*/
|
||||
exists(field: string): this {
|
||||
const existsQuery: ExistsQuery = {
|
||||
exists: {
|
||||
field,
|
||||
},
|
||||
};
|
||||
this.boolClauses.filter.push(existsQuery);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a prefix query
|
||||
*/
|
||||
prefix(field: string, value: string, boost?: number): this {
|
||||
const prefixQuery: PrefixQuery = {
|
||||
prefix: {
|
||||
[field]: {
|
||||
value,
|
||||
...(boost && { boost }),
|
||||
},
|
||||
},
|
||||
};
|
||||
this.boolClauses.must.push(prefixQuery);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a wildcard query
|
||||
*/
|
||||
wildcard(field: string, value: string, boost?: number): this {
|
||||
const wildcardQuery: WildcardQuery = {
|
||||
wildcard: {
|
||||
[field]: {
|
||||
value,
|
||||
...(boost && { boost }),
|
||||
},
|
||||
},
|
||||
};
|
||||
this.boolClauses.must.push(wildcardQuery);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a regexp query
|
||||
*/
|
||||
regexp(field: string, value: string, options?: { flags?: string; boost?: number }): this {
|
||||
const regexpQuery: RegexpQuery = {
|
||||
regexp: {
|
||||
[field]: {
|
||||
value,
|
||||
...options,
|
||||
},
|
||||
},
|
||||
};
|
||||
this.boolClauses.must.push(regexpQuery);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a fuzzy query
|
||||
*/
|
||||
fuzzy(field: string, value: string, options?: { fuzziness?: number | 'AUTO'; boost?: number }): this {
|
||||
const fuzzyQuery: FuzzyQuery = {
|
||||
fuzzy: {
|
||||
[field]: {
|
||||
value,
|
||||
...options,
|
||||
},
|
||||
},
|
||||
};
|
||||
this.boolClauses.must.push(fuzzyQuery);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an IDs query
|
||||
*/
|
||||
ids(values: string[]): this {
|
||||
const idsQuery: IdsQuery = {
|
||||
ids: {
|
||||
values,
|
||||
},
|
||||
};
|
||||
this.boolClauses.filter.push(idsQuery);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a query string query
|
||||
*/
|
||||
queryString(query: string, options?: { default_field?: string; fields?: string[]; default_operator?: MatchOperator; boost?: number }): this {
|
||||
const queryStringQuery: QueryStringQuery = {
|
||||
query_string: {
|
||||
query,
|
||||
...options,
|
||||
},
|
||||
};
|
||||
this.boolClauses.must.push(queryStringQuery);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a simple query string query
|
||||
*/
|
||||
simpleQueryString(query: string, options?: { fields?: string[]; default_operator?: MatchOperator; boost?: number }): this {
|
||||
const simpleQueryStringQuery: SimpleQueryStringQuery = {
|
||||
simple_query_string: {
|
||||
query,
|
||||
...options,
|
||||
},
|
||||
};
|
||||
this.boolClauses.must.push(simpleQueryStringQuery);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match all documents
|
||||
*/
|
||||
matchAll(boost?: number): this {
|
||||
const matchAllQuery: MatchAllQuery = {
|
||||
match_all: {
|
||||
...(boost && { boost }),
|
||||
},
|
||||
};
|
||||
this.queryDSL = matchAllQuery;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom query to the must clause
|
||||
*/
|
||||
must(query: QueryDSL): this {
|
||||
this.boolClauses.must.push(query);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom query to the should clause
|
||||
*/
|
||||
should(query: QueryDSL): this {
|
||||
this.boolClauses.should.push(query);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom query to the must_not clause
|
||||
*/
|
||||
mustNot(query: QueryDSL): this {
|
||||
this.boolClauses.must_not.push(query);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom query to the filter clause
|
||||
*/
|
||||
filter(query: QueryDSL): this {
|
||||
this.boolClauses.filter.push(query);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set minimum_should_match for boolean queries
|
||||
*/
|
||||
minimumMatch(value: number | string): this {
|
||||
this.minimumShouldMatch = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a custom query DSL (replaces builder queries)
|
||||
*/
|
||||
customQuery(query: QueryDSL): this {
|
||||
this.queryDSL = query;
|
||||
return this;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Result Shaping Methods
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Add sorting
|
||||
*/
|
||||
sort(field: string, order: SortOrder = 'asc'): this {
|
||||
this.sortFields.push({ [field]: { order } });
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom sort configuration
|
||||
*/
|
||||
customSort(sort: SortField | string): this {
|
||||
this.sortFields.push(sort);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify fields to include in results (source filtering)
|
||||
*/
|
||||
fields(fields: string[]): this {
|
||||
this.sourceFields = fields;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify fields to exclude from results
|
||||
*/
|
||||
exclude(fields: string[]): this {
|
||||
this.excludeSourceFields = fields;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set number of results to return
|
||||
*/
|
||||
size(size: number): this {
|
||||
this.resultSize = size;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set offset for pagination
|
||||
*/
|
||||
from(from: number): this {
|
||||
this.resultFrom = from;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set pagination (convenience method)
|
||||
*/
|
||||
paginate(page: number, pageSize: number): this {
|
||||
this.resultFrom = (page - 1) * pageSize;
|
||||
this.resultSize = pageSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to track total hits
|
||||
*/
|
||||
trackTotalHits(track: boolean | number): this {
|
||||
this.shouldTrackTotalHits = track;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set search timeout
|
||||
*/
|
||||
timeout(timeout: string): this {
|
||||
this.searchTimeout = timeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure highlighting
|
||||
*/
|
||||
highlight(config: SearchOptions['highlight']): this {
|
||||
this.highlightConfig = config;
|
||||
return this;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Aggregation Methods
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get aggregation builder
|
||||
*/
|
||||
aggregations(configure: (builder: AggregationBuilder) => void): this {
|
||||
if (!this.aggregationBuilder) {
|
||||
this.aggregationBuilder = createAggregationBuilder();
|
||||
}
|
||||
configure(this.aggregationBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Build & Execute
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Build the final query DSL
|
||||
*/
|
||||
build(): SearchOptions {
|
||||
let finalQuery: QueryDSL | undefined;
|
||||
|
||||
// If custom query was set, use it
|
||||
if (this.queryDSL) {
|
||||
finalQuery = this.queryDSL;
|
||||
} else {
|
||||
// Otherwise, build from bool clauses
|
||||
const hasAnyClauses =
|
||||
this.boolClauses.must.length > 0 ||
|
||||
this.boolClauses.should.length > 0 ||
|
||||
this.boolClauses.must_not.length > 0 ||
|
||||
this.boolClauses.filter.length > 0;
|
||||
|
||||
if (hasAnyClauses) {
|
||||
const boolQuery: BoolQuery = {
|
||||
bool: {},
|
||||
};
|
||||
|
||||
if (this.boolClauses.must.length > 0) {
|
||||
boolQuery.bool.must = this.boolClauses.must;
|
||||
}
|
||||
if (this.boolClauses.should.length > 0) {
|
||||
boolQuery.bool.should = this.boolClauses.should;
|
||||
}
|
||||
if (this.boolClauses.must_not.length > 0) {
|
||||
boolQuery.bool.must_not = this.boolClauses.must_not;
|
||||
}
|
||||
if (this.boolClauses.filter.length > 0) {
|
||||
boolQuery.bool.filter = this.boolClauses.filter;
|
||||
}
|
||||
if (this.minimumShouldMatch !== undefined) {
|
||||
boolQuery.bool.minimum_should_match = this.minimumShouldMatch;
|
||||
}
|
||||
|
||||
finalQuery = boolQuery;
|
||||
}
|
||||
}
|
||||
|
||||
const searchOptions: SearchOptions = {
|
||||
...(finalQuery && { query: finalQuery }),
|
||||
...(this.sourceFields && { fields: this.sourceFields }),
|
||||
...(this.excludeSourceFields && { excludeFields: this.excludeSourceFields }),
|
||||
size: this.resultSize,
|
||||
from: this.resultFrom,
|
||||
...(this.sortFields.length > 0 && { sort: this.sortFields }),
|
||||
trackTotalHits: this.shouldTrackTotalHits,
|
||||
...(this.searchTimeout && { timeout: this.searchTimeout }),
|
||||
...(this.highlightConfig && { highlight: this.highlightConfig }),
|
||||
...(this.aggregationBuilder && { aggregations: this.aggregationBuilder.build() }),
|
||||
};
|
||||
|
||||
return searchOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query and return results
|
||||
*/
|
||||
async execute(): Promise<SearchResult<T>> {
|
||||
const span = defaultTracing.createSpan('query.execute', {
|
||||
'db.system': 'elasticsearch',
|
||||
'db.operation': 'search',
|
||||
'db.elasticsearch.index': this.index,
|
||||
});
|
||||
|
||||
try {
|
||||
const client = ElasticsearchConnectionManager.getInstance().getClient();
|
||||
const searchOptions = this.build();
|
||||
|
||||
defaultLogger.debug('Executing query', {
|
||||
index: this.index,
|
||||
query: searchOptions.query,
|
||||
size: searchOptions.size,
|
||||
from: searchOptions.from,
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// Execute search
|
||||
const result = await client.search<T>({
|
||||
index: this.index,
|
||||
...searchOptions,
|
||||
});
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
// Record metrics
|
||||
defaultMetrics.requestsTotal.inc({ operation: 'search', index: this.index });
|
||||
defaultMetrics.requestDuration.observe({ operation: 'search', index: this.index }, duration);
|
||||
|
||||
defaultLogger.info('Query executed successfully', {
|
||||
index: this.index,
|
||||
took: result.took,
|
||||
hits: result.hits.total,
|
||||
duration,
|
||||
});
|
||||
|
||||
span.setAttributes({
|
||||
'db.elasticsearch.took': result.took,
|
||||
'db.elasticsearch.hits': typeof result.hits.total === 'object' ? result.hits.total.value : result.hits.total,
|
||||
});
|
||||
span.end();
|
||||
|
||||
return result as SearchResult<T>;
|
||||
} catch (error) {
|
||||
defaultMetrics.requestErrors.inc({ operation: 'search', index: this.index });
|
||||
defaultLogger.error('Query execution failed', { index: this.index, error: error instanceof Error ? error.message : String(error) });
|
||||
span.recordException(error as Error);
|
||||
span.end();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute query and return only the hits
|
||||
*/
|
||||
async executeAndGetHits(): Promise<SearchResult<T>['hits']['hits']> {
|
||||
const result = await this.execute();
|
||||
return result.hits.hits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute query and return only the source documents
|
||||
*/
|
||||
async executeAndGetSources(): Promise<T[]> {
|
||||
const hits = await this.executeAndGetHits();
|
||||
return hits.map((hit) => hit._source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count documents matching the query
|
||||
*/
|
||||
async count(): Promise<number> {
|
||||
const span = defaultTracing.createSpan('query.count', {
|
||||
'db.system': 'elasticsearch',
|
||||
'db.operation': 'count',
|
||||
'db.elasticsearch.index': this.index,
|
||||
});
|
||||
|
||||
try {
|
||||
const client = ElasticsearchConnectionManager.getInstance().getClient();
|
||||
const searchOptions = this.build();
|
||||
|
||||
const result = await client.count({
|
||||
index: this.index,
|
||||
...(searchOptions.query && { query: searchOptions.query }),
|
||||
});
|
||||
|
||||
span.end();
|
||||
return result.count;
|
||||
} catch (error) {
|
||||
span.recordException(error as Error);
|
||||
span.end();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new query builder instance
|
||||
*/
|
||||
export function createQuery<T>(index: string): QueryBuilder<T> {
|
||||
return new QueryBuilder<T>(index);
|
||||
}
|
||||
563
ts/domain/query/types.ts
Normal file
563
ts/domain/query/types.ts
Normal file
@@ -0,0 +1,563 @@
|
||||
/**
|
||||
* Query DSL type definitions for type-safe Elasticsearch queries
|
||||
*/
|
||||
|
||||
/**
|
||||
* Elasticsearch query types
|
||||
*/
|
||||
export type QueryType =
|
||||
| 'match'
|
||||
| 'match_phrase'
|
||||
| 'multi_match'
|
||||
| 'term'
|
||||
| 'terms'
|
||||
| 'range'
|
||||
| 'exists'
|
||||
| 'prefix'
|
||||
| 'wildcard'
|
||||
| 'regexp'
|
||||
| 'fuzzy'
|
||||
| 'ids'
|
||||
| 'bool'
|
||||
| 'match_all'
|
||||
| 'query_string'
|
||||
| 'simple_query_string';
|
||||
|
||||
/**
|
||||
* Boolean query clause types
|
||||
*/
|
||||
export type BoolClause = 'must' | 'should' | 'must_not' | 'filter';
|
||||
|
||||
/**
|
||||
* Sort order
|
||||
*/
|
||||
export type SortOrder = 'asc' | 'desc';
|
||||
|
||||
/**
|
||||
* Match query operator
|
||||
*/
|
||||
export type MatchOperator = 'or' | 'and';
|
||||
|
||||
/**
|
||||
* Multi-match type
|
||||
*/
|
||||
export type MultiMatchType =
|
||||
| 'best_fields'
|
||||
| 'most_fields'
|
||||
| 'cross_fields'
|
||||
| 'phrase'
|
||||
| 'phrase_prefix'
|
||||
| 'bool_prefix';
|
||||
|
||||
/**
|
||||
* Range query bounds
|
||||
*/
|
||||
export interface RangeBounds {
|
||||
gt?: number | string | Date;
|
||||
gte?: number | string | Date;
|
||||
lt?: number | string | Date;
|
||||
lte?: number | string | Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match query definition
|
||||
*/
|
||||
export interface MatchQuery {
|
||||
match: {
|
||||
[field: string]: {
|
||||
query: string;
|
||||
operator?: MatchOperator;
|
||||
fuzziness?: number | 'AUTO';
|
||||
boost?: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Match phrase query definition
|
||||
*/
|
||||
export interface MatchPhraseQuery {
|
||||
match_phrase: {
|
||||
[field: string]: {
|
||||
query: string;
|
||||
slop?: number;
|
||||
boost?: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi-match query definition
|
||||
*/
|
||||
export interface MultiMatchQuery {
|
||||
multi_match: {
|
||||
query: string;
|
||||
fields: string[];
|
||||
type?: MultiMatchType;
|
||||
operator?: MatchOperator;
|
||||
boost?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Term query definition
|
||||
*/
|
||||
export interface TermQuery {
|
||||
term: {
|
||||
[field: string]: {
|
||||
value: string | number | boolean;
|
||||
boost?: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Terms query definition
|
||||
*/
|
||||
export interface TermsQuery {
|
||||
terms: {
|
||||
[field: string]: Array<string | number | boolean>;
|
||||
boost?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Range query definition
|
||||
*/
|
||||
export interface RangeQuery {
|
||||
range: {
|
||||
[field: string]: RangeBounds & {
|
||||
boost?: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Exists query definition
|
||||
*/
|
||||
export interface ExistsQuery {
|
||||
exists: {
|
||||
field: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefix query definition
|
||||
*/
|
||||
export interface PrefixQuery {
|
||||
prefix: {
|
||||
[field: string]: {
|
||||
value: string;
|
||||
boost?: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Wildcard query definition
|
||||
*/
|
||||
export interface WildcardQuery {
|
||||
wildcard: {
|
||||
[field: string]: {
|
||||
value: string;
|
||||
boost?: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Regexp query definition
|
||||
*/
|
||||
export interface RegexpQuery {
|
||||
regexp: {
|
||||
[field: string]: {
|
||||
value: string;
|
||||
flags?: string;
|
||||
boost?: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuzzy query definition
|
||||
*/
|
||||
export interface FuzzyQuery {
|
||||
fuzzy: {
|
||||
[field: string]: {
|
||||
value: string;
|
||||
fuzziness?: number | 'AUTO';
|
||||
boost?: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* IDs query definition
|
||||
*/
|
||||
export interface IdsQuery {
|
||||
ids: {
|
||||
values: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Match all query definition
|
||||
*/
|
||||
export interface MatchAllQuery {
|
||||
match_all: {
|
||||
boost?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Query string query definition
|
||||
*/
|
||||
export interface QueryStringQuery {
|
||||
query_string: {
|
||||
query: string;
|
||||
default_field?: string;
|
||||
fields?: string[];
|
||||
default_operator?: MatchOperator;
|
||||
boost?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple query string query definition
|
||||
*/
|
||||
export interface SimpleQueryStringQuery {
|
||||
simple_query_string: {
|
||||
query: string;
|
||||
fields?: string[];
|
||||
default_operator?: MatchOperator;
|
||||
boost?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Boolean query definition
|
||||
*/
|
||||
export interface BoolQuery {
|
||||
bool: {
|
||||
must?: QueryDSL[];
|
||||
should?: QueryDSL[];
|
||||
must_not?: QueryDSL[];
|
||||
filter?: QueryDSL[];
|
||||
minimum_should_match?: number | string;
|
||||
boost?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Union of all query types
|
||||
*/
|
||||
export type QueryDSL =
|
||||
| MatchQuery
|
||||
| MatchPhraseQuery
|
||||
| MultiMatchQuery
|
||||
| TermQuery
|
||||
| TermsQuery
|
||||
| RangeQuery
|
||||
| ExistsQuery
|
||||
| PrefixQuery
|
||||
| WildcardQuery
|
||||
| RegexpQuery
|
||||
| FuzzyQuery
|
||||
| IdsQuery
|
||||
| MatchAllQuery
|
||||
| QueryStringQuery
|
||||
| SimpleQueryStringQuery
|
||||
| BoolQuery;
|
||||
|
||||
/**
|
||||
* Sort field definition
|
||||
*/
|
||||
export interface SortField {
|
||||
[field: string]: {
|
||||
order?: SortOrder;
|
||||
mode?: 'min' | 'max' | 'sum' | 'avg' | 'median';
|
||||
missing?: '_first' | '_last' | string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Search request options
|
||||
*/
|
||||
export interface SearchOptions {
|
||||
/** Query to execute */
|
||||
query?: QueryDSL;
|
||||
|
||||
/** Fields to return (source filtering) */
|
||||
fields?: string[];
|
||||
|
||||
/** Exclude source fields */
|
||||
excludeFields?: string[];
|
||||
|
||||
/** Number of results to return */
|
||||
size?: number;
|
||||
|
||||
/** Offset for pagination */
|
||||
from?: number;
|
||||
|
||||
/** Sort order */
|
||||
sort?: Array<SortField | string>;
|
||||
|
||||
/** Track total hits */
|
||||
trackTotalHits?: boolean | number;
|
||||
|
||||
/** Search timeout */
|
||||
timeout?: string;
|
||||
|
||||
/** Highlight configuration */
|
||||
highlight?: {
|
||||
fields: {
|
||||
[field: string]: {
|
||||
pre_tags?: string[];
|
||||
post_tags?: string[];
|
||||
fragment_size?: number;
|
||||
number_of_fragments?: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/** Aggregations */
|
||||
aggregations?: Record<string, AggregationDSL>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregation types
|
||||
*/
|
||||
export type AggregationType =
|
||||
| 'terms'
|
||||
| 'avg'
|
||||
| 'sum'
|
||||
| 'min'
|
||||
| 'max'
|
||||
| 'cardinality'
|
||||
| 'stats'
|
||||
| 'extended_stats'
|
||||
| 'percentiles'
|
||||
| 'date_histogram'
|
||||
| 'histogram'
|
||||
| 'range'
|
||||
| 'filter'
|
||||
| 'filters'
|
||||
| 'nested'
|
||||
| 'reverse_nested'
|
||||
| 'top_hits';
|
||||
|
||||
/**
|
||||
* Terms aggregation
|
||||
*/
|
||||
export interface TermsAggregation {
|
||||
terms: {
|
||||
field: string;
|
||||
size?: number;
|
||||
order?: Record<string, SortOrder>;
|
||||
missing?: string | number;
|
||||
};
|
||||
aggs?: Record<string, AggregationDSL>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metric aggregations (avg, sum, min, max, cardinality)
|
||||
*/
|
||||
export interface MetricAggregation {
|
||||
avg?: { field: string; missing?: number };
|
||||
sum?: { field: string; missing?: number };
|
||||
min?: { field: string; missing?: number };
|
||||
max?: { field: string; missing?: number };
|
||||
cardinality?: { field: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* Stats aggregation
|
||||
*/
|
||||
export interface StatsAggregation {
|
||||
stats: {
|
||||
field: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended stats aggregation
|
||||
*/
|
||||
export interface ExtendedStatsAggregation {
|
||||
extended_stats: {
|
||||
field: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Percentiles aggregation
|
||||
*/
|
||||
export interface PercentilesAggregation {
|
||||
percentiles: {
|
||||
field: string;
|
||||
percents?: number[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Date histogram aggregation
|
||||
*/
|
||||
export interface DateHistogramAggregation {
|
||||
date_histogram: {
|
||||
field: string;
|
||||
calendar_interval?: string;
|
||||
fixed_interval?: string;
|
||||
format?: string;
|
||||
time_zone?: string;
|
||||
min_doc_count?: number;
|
||||
};
|
||||
aggs?: Record<string, AggregationDSL>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Histogram aggregation
|
||||
*/
|
||||
export interface HistogramAggregation {
|
||||
histogram: {
|
||||
field: string;
|
||||
interval: number;
|
||||
min_doc_count?: number;
|
||||
};
|
||||
aggs?: Record<string, AggregationDSL>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Range aggregation
|
||||
*/
|
||||
export interface RangeAggregation {
|
||||
range: {
|
||||
field: string;
|
||||
ranges: Array<{ from?: number; to?: number; key?: string }>;
|
||||
};
|
||||
aggs?: Record<string, AggregationDSL>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter aggregation
|
||||
*/
|
||||
export interface FilterAggregation {
|
||||
filter: QueryDSL;
|
||||
aggs?: Record<string, AggregationDSL>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Top hits aggregation
|
||||
*/
|
||||
export interface TopHitsAggregation {
|
||||
top_hits: {
|
||||
size?: number;
|
||||
sort?: Array<SortField | string>;
|
||||
_source?: boolean | { includes?: string[]; excludes?: string[] };
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Union of all aggregation types
|
||||
*/
|
||||
export type AggregationDSL =
|
||||
| TermsAggregation
|
||||
| MetricAggregation
|
||||
| StatsAggregation
|
||||
| ExtendedStatsAggregation
|
||||
| PercentilesAggregation
|
||||
| DateHistogramAggregation
|
||||
| HistogramAggregation
|
||||
| RangeAggregation
|
||||
| FilterAggregation
|
||||
| TopHitsAggregation;
|
||||
|
||||
/**
|
||||
* Search result hit
|
||||
*/
|
||||
export interface SearchHit<T> {
|
||||
_index: string;
|
||||
_id: string;
|
||||
_score: number | null;
|
||||
_source: T;
|
||||
fields?: Record<string, unknown[]>;
|
||||
highlight?: Record<string, string[]>;
|
||||
sort?: Array<string | number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregation bucket
|
||||
*/
|
||||
export interface AggregationBucket {
|
||||
key: string | number;
|
||||
key_as_string?: string;
|
||||
doc_count: number;
|
||||
[aggName: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Terms aggregation result
|
||||
*/
|
||||
export interface TermsAggregationResult {
|
||||
doc_count_error_upper_bound: number;
|
||||
sum_other_doc_count: number;
|
||||
buckets: AggregationBucket[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Metric aggregation result
|
||||
*/
|
||||
export interface MetricAggregationResult {
|
||||
value: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stats aggregation result
|
||||
*/
|
||||
export interface StatsAggregationResult {
|
||||
count: number;
|
||||
min: number;
|
||||
max: number;
|
||||
avg: number;
|
||||
sum: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Percentiles aggregation result
|
||||
*/
|
||||
export interface PercentilesAggregationResult {
|
||||
values: Record<string, number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic aggregation result
|
||||
*/
|
||||
export type AggregationResult =
|
||||
| TermsAggregationResult
|
||||
| MetricAggregationResult
|
||||
| StatsAggregationResult
|
||||
| PercentilesAggregationResult
|
||||
| { buckets: AggregationBucket[] }
|
||||
| { value: number }
|
||||
| Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Search result
|
||||
*/
|
||||
export interface SearchResult<T> {
|
||||
took: number;
|
||||
timed_out: boolean;
|
||||
_shards: {
|
||||
total: number;
|
||||
successful: number;
|
||||
skipped: number;
|
||||
failed: number;
|
||||
};
|
||||
hits: {
|
||||
total: {
|
||||
value: number;
|
||||
relation: 'eq' | 'gte';
|
||||
};
|
||||
max_score: number | null;
|
||||
hits: SearchHit<T>[];
|
||||
};
|
||||
aggregations?: Record<string, AggregationResult>;
|
||||
}
|
||||
Reference in New Issue
Block a user