diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0cd1d3f7..6045b1cd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,6 +10,8 @@ First of all, thank you for contributing to Meilisearch! The goal of this docume - [Setup ](#setup-) - [Tests and Linter ](#tests-and-linter-) - [Updating code samples](#updating-code-samples) + - [Run integration tests for embedders](#run-integration-tests-for-embedders) + - [OpenAI Model Integration](#openai-model-integration) - [Git Guidelines](#git-guidelines) - [Git Branches ](#git-branches-) - [Git Commits ](#git-commits-) @@ -103,6 +105,19 @@ The process to define a new code sample is as follows: ```bash dart run ./tool/bin/meili.dart update-samples --fail-on-change ``` + +### Run integration tests for embedders + +Integration tests for embedders are located in `test/search_test.dart` + +#### OpenAI Model Integration +The tests utilize OpenAI models for embedding functionalities. Ensure you have a valid OpenAI API key to run these tests. + +- Generate an OpenAI API Key +- Provide the API Key in one of two ways: + - Pass the key via environment variable: `export OPEN_AI_API_KEY=your_openai_api_key` (will not work on dart web) + - Pass the key via dart define: `dart --define=OPEN_AI_API_KEY=your_openai_api_key test --use-data-isolate-strategy` (Works on both web and native) + ## Git Guidelines ### Git Branches diff --git a/README.md b/README.md index ad815dc1..d57212f6 100644 --- a/README.md +++ b/README.md @@ -210,8 +210,6 @@ final res = await index This package guarantees compatibility with [version v1.x of Meilisearch](https://github.com/meilisearch/meilisearch/releases/tag/latest), but some features may not be present. Please check the [issues](https://github.com/meilisearch/meilisearch-dart/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+label%3Aenhancement) for more info. -⚠️ This package is not compatible with the [`vectoreStore` experimental feature](https://www.meilisearch.com/docs/learn/experimental/vector_search) of Meilisearch v1.6.0 and later. More information on this [issue](https://github.com/meilisearch/meilisearch-dart/issues/369). - ## 💡 Learn more The following sections in our main documentation website may interest you: diff --git a/lib/meilisearch.dart b/lib/meilisearch.dart index b66dc93a..d5dbe2b9 100644 --- a/lib/meilisearch.dart +++ b/lib/meilisearch.dart @@ -8,3 +8,5 @@ export 'src/client.dart'; export 'src/index.dart'; export 'src/exception.dart'; export 'src/facade.dart'; +export 'src/settings/distribution.dart'; +export 'src/settings/embedder.dart'; diff --git a/lib/src/index.dart b/lib/src/index.dart index 47aadca1..cf3c53e3 100644 --- a/lib/src/index.dart +++ b/lib/src/index.dart @@ -685,6 +685,35 @@ class MeiliSearchIndex { ); } + /// Get the embedders settings of a Meilisearch index. + @RequiredMeiliServerVersion('1.6.0') + Future?> getEmbedders() async { + final response = await http + .getMethod>('/indexes/$uid/settings/embedders'); + + return response.data?.map( + (k, v) => MapEntry(k, Embedder.fromMap(v as Map))); + } + + /// Update the embedders settings. Overwrite the old settings. + @RequiredMeiliServerVersion('1.6.0') + Future updateEmbedders(Map? embedders) async { + return await _getTask( + http.putMethod( + '/indexes/$uid/settings/embedders', + data: embedders?.map((k, v) => MapEntry(k, v.toMap())), + ), + ); + } + + /// Reset the embedders settings to its default value + @RequiredMeiliServerVersion('1.6.0') + Future resetEmbedders() async { + return await _getTask( + http.deleteMethod('/indexes/$uid/settings/embedders'), + ); + } + // // StopWords endpoints // diff --git a/lib/src/query_parameters/_exports.dart b/lib/src/query_parameters/_exports.dart index 503df7cf..f0ea00da 100644 --- a/lib/src/query_parameters/_exports.dart +++ b/lib/src/query_parameters/_exports.dart @@ -11,3 +11,4 @@ export 'multi_search_query.dart'; export 'delete_documents_query.dart'; export 'facet_search_query.dart'; export 'swap_index.dart'; +export 'hybrid_search.dart'; diff --git a/lib/src/query_parameters/hybrid_search.dart b/lib/src/query_parameters/hybrid_search.dart new file mode 100644 index 00000000..e5a2965d --- /dev/null +++ b/lib/src/query_parameters/hybrid_search.dart @@ -0,0 +1,17 @@ +class HybridSearch { + final String embedder; + final double semanticRatio; + + const HybridSearch({ + required this.embedder, + required this.semanticRatio, + }) : assert( + semanticRatio >= 0.0 && semanticRatio <= 1.0, + "'semanticRatio' must be between 0.0 and 1.0", + ); + + Map toMap() => { + 'embedder': embedder, + 'semanticRatio': semanticRatio, + }; +} diff --git a/lib/src/query_parameters/index_search_query.dart b/lib/src/query_parameters/index_search_query.dart index 5ae3cb28..de00e510 100644 --- a/lib/src/query_parameters/index_search_query.dart +++ b/lib/src/query_parameters/index_search_query.dart @@ -1,6 +1,7 @@ import '../annotations.dart'; import '../filter_builder/_exports.dart'; import '../results/matching_strategy_enum.dart'; +import 'hybrid_search.dart'; import 'search_query.dart'; @RequiredMeiliServerVersion('1.1.0') @@ -29,6 +30,7 @@ class IndexSearchQuery extends SearchQuery { super.highlightPostTag, super.matchingStrategy, super.attributesToSearchOn, + super.hybrid, super.showRankingScore, super.vector, super.showRankingScoreDetails, @@ -65,6 +67,7 @@ class IndexSearchQuery extends SearchQuery { String? highlightPostTag, MatchingStrategy? matchingStrategy, List? attributesToSearchOn, + HybridSearch? hybrid, bool? showRankingScore, List */ >? vector, bool? showRankingScoreDetails, @@ -91,6 +94,7 @@ class IndexSearchQuery extends SearchQuery { highlightPostTag: highlightPostTag ?? this.highlightPostTag, matchingStrategy: matchingStrategy ?? this.matchingStrategy, attributesToSearchOn: attributesToSearchOn ?? this.attributesToSearchOn, + hybrid: hybrid ?? this.hybrid, showRankingScore: showRankingScore ?? this.showRankingScore, vector: vector ?? this.vector, showRankingScoreDetails: diff --git a/lib/src/query_parameters/search_query.dart b/lib/src/query_parameters/search_query.dart index 27918ab3..7c1100f3 100644 --- a/lib/src/query_parameters/search_query.dart +++ b/lib/src/query_parameters/search_query.dart @@ -21,6 +21,8 @@ class SearchQuery extends Queryable { final String? highlightPostTag; final MatchingStrategy? matchingStrategy; final List? attributesToSearchOn; + @RequiredMeiliServerVersion('1.6.0') + final HybridSearch? hybrid; @RequiredMeiliServerVersion('1.3.0') final bool? showRankingScore; @RequiredMeiliServerVersion('1.3.0') @@ -47,6 +49,7 @@ class SearchQuery extends Queryable { this.highlightPostTag, this.matchingStrategy, this.attributesToSearchOn, + this.hybrid, this.showRankingScore, this.showRankingScoreDetails, this.vector, @@ -72,6 +75,7 @@ class SearchQuery extends Queryable { 'highlightPostTag': highlightPostTag, 'matchingStrategy': matchingStrategy?.name, 'attributesToSearchOn': attributesToSearchOn, + 'hybrid': hybrid?.toMap(), 'showRankingScore': showRankingScore, 'showRankingScoreDetails': showRankingScoreDetails, 'vector': vector, @@ -97,6 +101,7 @@ class SearchQuery extends Queryable { String? highlightPostTag, MatchingStrategy? matchingStrategy, List? attributesToSearchOn, + HybridSearch? hybrid, bool? showRankingScore, List? vector, bool? showRankingScoreDetails, @@ -121,6 +126,7 @@ class SearchQuery extends Queryable { highlightPostTag: highlightPostTag ?? this.highlightPostTag, matchingStrategy: matchingStrategy ?? this.matchingStrategy, attributesToSearchOn: attributesToSearchOn ?? this.attributesToSearchOn, + hybrid: hybrid ?? this.hybrid, showRankingScore: showRankingScore ?? this.showRankingScore, vector: vector ?? this.vector, showRankingScoreDetails: diff --git a/lib/src/settings/distribution.dart b/lib/src/settings/distribution.dart new file mode 100644 index 00000000..9cb880a3 --- /dev/null +++ b/lib/src/settings/distribution.dart @@ -0,0 +1,31 @@ +/// Describes the mean and sigma of distribution of embedding similarity in the embedding space. +/// +/// The intended use is to make the similarity score more comparable to the regular ranking score. +/// This allows to correct effects where results are too "packed" around a certain value. +class DistributionShift { + /// Value where the results are "packed". + /// Similarity scores are translated so that they are packed around 0.5 instead + final double currentMean; + + /// standard deviation of a similarity score. + /// + /// Set below 0.4 to make the results less packed around the mean, and above 0.4 to make them more packed. + final double currentSigma; + + DistributionShift({ + required this.currentMean, + required this.currentSigma, + }); + + factory DistributionShift.fromMap(Map map) { + return DistributionShift( + currentMean: map['current_mean'] as double, + currentSigma: map['current_sigma'] as double, + ); + } + + Map toMap() => { + 'current_mean': currentMean, + 'current_sigma': currentSigma, + }; +} diff --git a/lib/src/settings/embedder.dart b/lib/src/settings/embedder.dart new file mode 100644 index 00000000..a620e8f0 --- /dev/null +++ b/lib/src/settings/embedder.dart @@ -0,0 +1,278 @@ +import './distribution.dart'; + +abstract class Embedder { + const Embedder(); + + Map toMap(); + + factory Embedder.fromMap(Map map) { + final source = map['source']; + + return switch (source) { + OpenAiEmbedder.source => OpenAiEmbedder.fromMap(map), + HuggingFaceEmbedder.source => HuggingFaceEmbedder.fromMap(map), + UserProvidedEmbedder.source => UserProvidedEmbedder.fromMap(map), + RestEmbedder.source => RestEmbedder.fromMap(map), + OllamaEmbedder.source => OllamaEmbedder.fromMap(map), + _ => UnknownEmbedder(data: map), + }; + } +} + +class OpenAiEmbedder extends Embedder { + static const source = 'openAi'; + final String? model; + final String? apiKey; + final String? documentTemplate; + final int? dimensions; + final DistributionShift? distribution; + final String? url; + final int? documentTemplateMaxBytes; + final bool? binaryQuantized; + + const OpenAiEmbedder({ + this.model, + this.apiKey, + this.documentTemplate, + this.dimensions, + this.distribution, + this.url, + this.documentTemplateMaxBytes, + this.binaryQuantized, + }); + + @override + Map toMap() => { + 'source': source, + 'model': model, + 'apiKey': apiKey, + 'documentTemplate': documentTemplate, + 'dimensions': dimensions, + 'distribution': distribution?.toMap(), + 'url': url, + 'documentTemplateMaxBytes': documentTemplateMaxBytes, + 'binaryQuantized': binaryQuantized, + }; + + factory OpenAiEmbedder.fromMap(Map map) { + final distribution = map['distribution']; + + return OpenAiEmbedder( + model: map['model'] as String?, + apiKey: map['apiKey'] as String?, + documentTemplate: map['documentTemplate'] as String?, + dimensions: map['dimensions'] as int?, + distribution: distribution is Map + ? DistributionShift.fromMap(distribution) + : null, + url: map['url'] as String?, + documentTemplateMaxBytes: map['documentTemplateMaxBytes'] as int?, + binaryQuantized: map['binaryQuantized'] as bool?, + ); + } +} + +class HuggingFaceEmbedder extends Embedder { + static const source = 'huggingFace'; + final String? model; + final String? revision; + final String? documentTemplate; + final DistributionShift? distribution; + final int? documentTemplateMaxBytes; + final bool? binaryQuantized; + + const HuggingFaceEmbedder({ + this.model, + this.revision, + this.documentTemplate, + this.distribution, + this.documentTemplateMaxBytes, + this.binaryQuantized, + }); + + @override + Map toMap() => { + 'source': source, + 'model': model, + 'documentTemplate': documentTemplate, + 'distribution': distribution?.toMap(), + 'documentTemplateMaxBytes': documentTemplateMaxBytes, + 'binaryQuantized': binaryQuantized, + }; + + factory HuggingFaceEmbedder.fromMap(Map map) { + final distribution = map['distribution']; + + return HuggingFaceEmbedder( + model: map['model'] as String?, + documentTemplate: map['documentTemplate'] as String?, + distribution: distribution is Map + ? DistributionShift.fromMap(distribution) + : null, + documentTemplateMaxBytes: map['documentTemplateMaxBytes'] as int?, + binaryQuantized: map['binaryQuantized'] as bool?, + ); + } +} + +class UserProvidedEmbedder extends Embedder { + static const source = 'userProvided'; + final int dimensions; + final DistributionShift? distribution; + final bool? binaryQuantized; + + const UserProvidedEmbedder({ + required this.dimensions, + this.distribution, + this.binaryQuantized, + }); + + @override + Map toMap() => { + 'source': source, + 'dimensions': dimensions, + 'distribution': distribution?.toMap(), + 'binaryQuantized': binaryQuantized, + }; + + factory UserProvidedEmbedder.fromMap(Map map) { + final distribution = map['distribution']; + + return UserProvidedEmbedder( + dimensions: map['dimensions'] as int, + distribution: distribution is Map + ? DistributionShift.fromMap(distribution) + : null, + binaryQuantized: map['binaryQuantized'] as bool?, + ); + } +} + +class RestEmbedder extends Embedder { + static const source = 'rest'; + final String url; + final Map request; + final Map response; + final String? apiKey; + final int? dimensions; + final String? documentTemplate; + final DistributionShift? distribution; + final Map? headers; + final int? documentTemplateMaxBytes; + final bool? binaryQuantized; + + const RestEmbedder({ + required this.url, + required this.request, + required this.response, + this.apiKey, + this.dimensions, + this.documentTemplate, + this.distribution, + this.headers, + this.documentTemplateMaxBytes, + this.binaryQuantized, + }); + + @override + Map toMap() => { + 'source': source, + 'url': url, + 'request': request, + 'response': response, + 'apiKey': apiKey, + 'dimensions': dimensions, + 'documentTemplate': documentTemplate, + 'distribution': distribution?.toMap(), + 'headers': headers, + 'documentTemplateMaxBytes': documentTemplateMaxBytes, + 'binaryQuantized': binaryQuantized, + }; + + factory RestEmbedder.fromMap(Map map) { + final distribution = map['distribution']; + + return RestEmbedder( + url: map['url'] as String, + request: map['request'] as Map, + response: map['response'] as Map, + apiKey: map['apiKey'] as String?, + dimensions: map['dimensions'] as int?, + documentTemplate: map['documentTemplate'] as String?, + distribution: distribution is Map + ? DistributionShift.fromMap(distribution) + : null, + headers: map['headers'] as Map?, + documentTemplateMaxBytes: map['documentTemplateMaxBytes'] as int?, + binaryQuantized: map['binaryQuantized'] as bool?, + ); + } +} + +class OllamaEmbedder extends Embedder { + static const source = 'ollama'; + final String? url; + final String? apiKey; + final String? model; + final String? documentTemplate; + final DistributionShift? distribution; + final int? dimensions; + final int? documentTemplateMaxBytes; + final bool? binaryQuantized; + + const OllamaEmbedder({ + this.url, + this.apiKey, + this.model, + this.documentTemplate, + this.distribution, + this.dimensions, + this.documentTemplateMaxBytes, + this.binaryQuantized, + }); + + @override + Map toMap() => { + 'source': source, + 'url': url, + 'apiKey': apiKey, + 'model': model, + 'documentTemplate': documentTemplate, + 'distribution': distribution?.toMap(), + 'dimensions': dimensions, + 'documentTemplateMaxBytes': documentTemplateMaxBytes, + 'binaryQuantized': binaryQuantized, + }; + + factory OllamaEmbedder.fromMap(Map map) { + final distribution = map['distribution']; + + return OllamaEmbedder( + url: map['url'] as String?, + apiKey: map['apiKey'] as String?, + model: map['model'] as String?, + documentTemplate: map['documentTemplate'] as String?, + distribution: distribution is Map + ? DistributionShift.fromMap(distribution) + : null, + dimensions: map['dimensions'] as int?, + documentTemplateMaxBytes: map['documentTemplateMaxBytes'] as int?, + binaryQuantized: map['binaryQuantized'] as bool?, + ); + } +} + +class UnknownEmbedder extends Embedder { + final Map data; + + const UnknownEmbedder({ + required this.data, + }); + + @override + Map toMap() => data; + + factory UnknownEmbedder.fromMap(String source, Map map) { + return UnknownEmbedder(data: map); + } +} diff --git a/lib/src/settings/index_settings.dart b/lib/src/settings/index_settings.dart index b40fc451..d84aa652 100644 --- a/lib/src/settings/index_settings.dart +++ b/lib/src/settings/index_settings.dart @@ -1,3 +1,5 @@ +import '../annotations.dart'; +import 'embedder.dart'; import 'faceting.dart'; import 'pagination.dart'; import 'typo_tolerance.dart'; @@ -17,6 +19,7 @@ class IndexSettings { this.faceting, this.separatorTokens, this.nonSeparatorTokens, + this.embedders, }); static const allAttributes = ['*']; @@ -60,6 +63,10 @@ class IndexSettings { ///Customize faceting feature. Faceting? faceting; + /// Set of embedders + @RequiredMeiliServerVersion('1.6.0') + Map? embedders; + Map toMap() => { 'synonyms': synonyms, 'stopWords': stopWords, @@ -73,7 +80,8 @@ class IndexSettings { 'pagination': pagination?.toMap(), 'faceting': faceting?.toMap(), 'separatorTokens': separatorTokens, - 'nonSeparatorTokens': nonSeparatorTokens + 'nonSeparatorTokens': nonSeparatorTokens, + 'embedders': embedders?.map((k, v) => MapEntry(k, v.toMap())), }; factory IndexSettings.fromMap(Map map) { @@ -89,6 +97,7 @@ class IndexSettings { final sortableAttributes = map['sortableAttributes']; final separatorTokens = map['separatorTokens']; final nonSeparatorTokens = map['nonSeparatorTokens']; + final embedders = map['embedders']; return IndexSettings( synonyms: synonyms is Map @@ -127,6 +136,10 @@ class IndexSettings { separatorTokens: separatorTokens is List ? separatorTokens.cast() : null, + embedders: embedders is Map + ? embedders.map((k, v) => + MapEntry(k, Embedder.fromMap(v as Map))) + : null, ); } } diff --git a/pubspec.yaml b/pubspec.yaml index 26dde90f..a306f7c8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,6 +21,7 @@ dev_dependencies: lints: ">=2.1.0 <4.0.0" json_serializable: ^6.7.1 build_runner: ^2.4.6 + pub_semver: ^2.1.5 screenshots: - description: The Meilisearch logo. diff --git a/test/search_test.dart b/test/search_test.dart index f304e12b..e68a2830 100644 --- a/test/search_test.dart +++ b/test/search_test.dart @@ -1,4 +1,5 @@ import 'package:meilisearch/meilisearch.dart'; +import 'package:meilisearch/src/results/experimental_features.dart'; import 'package:test/test.dart'; import 'utils/books.dart'; @@ -541,54 +542,168 @@ void main() { }); // Commented because of https://github.com/meilisearch/meilisearch-dart/issues/369 - // group('Experimental', () { - // setUpClient(); - // late String uid; - // late MeiliSearchIndex index; - // late ExperimentalFeatures features; - // setUp(() async { - // features = await client.http.updateExperimentalFeatures( - // UpdateExperimentalFeatures( - // vectorStore: true, - // ), - // ); - // expect(features.vectorStore, true); - - // uid = randomUid(); - // index = await createIndexWithData(uid: uid, data: vectorBooks); - // }); - - // test('vector search', () async { - // final vector = [0, 1, 2]; - // final res = await index - // .search( - // null, - // SearchQuery( - // vector: vector, - // ), - // ) - // .asSearchResult() - // .mapToContainer(); - - // expect(res.vector, vector); - // expect( - // res.hits, - // everyElement( - // isA>>() - // .having( - // (p0) => p0.vectors, - // 'vectors', - // isNotNull, - // ) - // .having( - // (p0) => p0.semanticScore, - // 'semanticScore', - // isNotNull, - // ), - // ), - // ); - // }); - // }); + group('Experimental', () { + setUpClient(); + late String uid; + late MeiliSearchIndex index; + late ExperimentalFeatures features; + setUp(() async { + features = await client.http.updateExperimentalFeatures( + UpdateExperimentalFeatures( + vectorStore: true, + ), + ); + expect(features.vectorStore, true); + + uid = randomUid(); + index = await createIndexWithData(uid: uid, data: vectorBooks); + }); + + test('vector search', () async { + final vector = [0, 1, 2]; + final res = await index + .search( + null, + SearchQuery( + vector: vector, + ), + ) + .asSearchResult() + .mapToContainer(); + + expect(res.vector, vector); + expect( + res.hits, + everyElement( + isA>>() + .having( + (p0) => p0.vectors, + 'vectors', + isNotNull, + ) + .having( + (p0) => p0.semanticScore, + 'semanticScore', + isNotNull, + ), + ), + ); + }); + }, skip: "Requires Experimental API"); + final openAiKeyValue = openAiKey; + group('Embedders', () { + group( + 'Unit test', + () { + // test serialization of models + test(OpenAiEmbedder, () { + final embedder = OpenAiEmbedder( + model: 'text-embedding-3-small', + apiKey: 'key', + documentTemplate: 'a book titled {{ doc.title }}', + binaryQuantized: true, + dimensions: 100, + distribution: DistributionShift( + currentMean: 20, + currentSigma: 5, + ), + url: 'https://example.com', + documentTemplateMaxBytes: 200, + ); + + final map = embedder.toMap(); + + expect(map, { + 'model': 'text-embedding-3-small', + 'apiKey': 'key', + 'documentTemplate': 'a book titled {{ doc.title }}', + 'dimensions': 100, + 'distribution': { + 'current_mean': 20, + 'current_sigma': 5, + }, + 'url': 'https://example.com', + 'documentTemplateMaxBytes': 200, + 'binaryQuantized': true, + 'source': 'openAi', + }); + + final deserialized = OpenAiEmbedder.fromMap(map); + + expect(deserialized.model, 'text-embedding-3-small'); + expect(deserialized.apiKey, 'key'); + expect( + deserialized.documentTemplate, 'a book titled {{ doc.title }}'); + expect(deserialized.dimensions, 100); + expect(deserialized.distribution?.currentMean, 20); + expect(deserialized.distribution?.currentSigma, 5); + expect(deserialized.url, 'https://example.com'); + expect(deserialized.documentTemplateMaxBytes, 200); + expect(deserialized.binaryQuantized, true); + }); + + test(HuggingFaceEmbedder, () {}); + }, + ); + + group( + 'Integration test', + () { + setUpClient(); + late String uid; + late MeiliSearchIndex index; + late IndexSettings settings; + + setUpAll(() { + settings = IndexSettings(embedders: { + 'default': OpenAiEmbedder( + model: 'text-embedding-3-small', + apiKey: openAiKeyValue, + documentTemplate: "a book titled '{{ doc.title }}'", + ), + }); + }); + + setUp(() async { + final features = await client.http.updateExperimentalFeatures( + UpdateExperimentalFeatures(vectorStore: true)); + expect(features.vectorStore, true); + uid = randomUid(); + index = await createBooksIndex(uid: uid); + }); + + test('set embedders', () async { + final result = + await index.updateSettings(settings).waitFor(client: client); + + expect(result.status, 'succeeded'); + }); + + test('reset embedders', () async { + final embedderResult = + await index.resetEmbedders().waitFor(client: client); + + expect(embedderResult.status, 'succeeded'); + }); + + test('hybrid search', () async { + final settingsResult = + await index.updateSettings(settings).waitFor(client: client); + + final sQuery = SearchQuery( + hybrid: HybridSearch(embedder: 'default', semanticRatio: 0.9)); + + final searchResult = await index.search('prince', sQuery); + + expect(settingsResult.status, 'succeeded'); + expect(searchResult.hits, hasLength(7)); + }); + }, + skip: openAiKeyValue == null || openAiKeyValue.isEmpty + ? "Requires OPEN_AI_API_KEY environment variable" + : null, + ); + }); test('search code samples', () async { // #docregion search_get_1 diff --git a/test/utils/client.dart b/test/utils/client.dart index 1439a75f..52989e7c 100644 --- a/test/utils/client.dart +++ b/test/utils/client.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:crypto/crypto.dart'; import 'package:meilisearch/src/http_request.dart'; +import 'package:pub_semver/pub_semver.dart'; import 'package:test/test.dart'; import '../models/test_client.dart'; @@ -26,6 +27,38 @@ String get testApiKey { return 'masterKey'; } +Version? get meiliServerVersion { + const meilisearchVersionKey = 'MEILISEARCH_VERSION'; + String? compileTimeValue = String.fromEnvironment(meilisearchVersionKey); + if (compileTimeValue.isEmpty) { + compileTimeValue = null; + } + if (_kIsWeb) { + if (compileTimeValue != null) { + return Version.parse(compileTimeValue); + } + } else { + var str = Platform.environment[meilisearchVersionKey] ?? compileTimeValue; + if (str != null && str.isNotEmpty) { + return Version.parse(str); + } + } + return null; +} + +String? get openAiKey { + const keyName = "OPEN_AI_API_KEY"; + String? compileTimeValue = String.fromEnvironment(keyName); + if (compileTimeValue.isEmpty) { + compileTimeValue = null; + } + if (_kIsWeb) { + return compileTimeValue; + } else { + return Platform.environment[keyName] ?? compileTimeValue; + } +} + void setUpClient() { setUp(() { client = TestMeiliSearchClient(testServer, testApiKey);