Skip to content

Commit 87c39a4

Browse files
authored
AI Search - Full text search (wip) (#74)
* AI Search - Full text search (wip) * AI Search - full-text search * remove unused type * cleanup * fix search terms * edit * Cleanup * edit
1 parent d3d476d commit 87c39a4

File tree

6 files changed

+525
-0
lines changed

6 files changed

+525
-0
lines changed

quickstarts/ai-search/.env.sample

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
SEARCH_API_KEY=<YOUR-SEARCH-ADMIN-API-KEY>
2+
SEARCH_API_ENDPOINT=<YOUR-SEARCH-SERVICE-URL>

quickstarts/ai-search/package.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "ai-search-full-text-search-quickstart",
3+
"version": "1.0.0",
4+
"type": "module",
5+
"scripts": {
6+
"test": "echo \"Error: no test specified\" && exit 1",
7+
"build": "tsc",
8+
"prettier:fix": "prettier --write \"src/**/*.ts\""
9+
},
10+
"author": "",
11+
"license": "ISC",
12+
"description": "",
13+
"dependencies": {
14+
"@azure/identity": "^4.3.0",
15+
"@azure/search-documents": "^12.1.0",
16+
"dotenv": "^16.4.5"
17+
},
18+
"devDependencies": {
19+
"@types/node": "^20.12.2",
20+
"@typescript-eslint/parser": "^5.62.0",
21+
"eslint": "^8.57.0",
22+
"prettier": "^3.3.3",
23+
"prettier-eslint": "^16.3.0",
24+
"ts-node": "^10.9.2",
25+
"typescript": "^5.4.3"
26+
},
27+
"prettier": {
28+
"printWidth": 80
29+
}
30+
}
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import {
2+
SearchIndexClient,
3+
SimpleField,
4+
ComplexField,
5+
SearchSuggester,
6+
SearchClient,
7+
SearchDocumentsResult,
8+
AzureKeyCredential,
9+
odata,
10+
SearchFieldArray,
11+
SearchIndex,
12+
} from "@azure/search-documents";
13+
import "dotenv/config";
14+
15+
// Import data
16+
import indexDefinition from "./hotels_quickstart_index.json" with { type: "json" };
17+
import hotelData from "./hotels.json" with { type: "json" };
18+
19+
// Get endpoint and apiKey from .env file
20+
const endpoint: string = process.env.SEARCH_API_ENDPOINT!!;
21+
const apiKey: string = process.env.SEARCH_API_KEY!!;
22+
if (!endpoint || !apiKey) {
23+
throw new Error(
24+
"Make sure to set valid values for endpoint and apiKey with proper authorization.",
25+
);
26+
}
27+
28+
function printSearchIndex(searchIndex: SearchIndex) {
29+
const { name, etag, defaultScoringProfile } = searchIndex;
30+
console.log(
31+
`Search Index name: ${name}, etag: ${etag}, defaultScoringProfile: ${defaultScoringProfile}`,
32+
);
33+
}
34+
function wait(ms: number): Promise<void> {
35+
return new Promise((resolve) => setTimeout(resolve, ms));
36+
}
37+
38+
type MyIndexDefinition = {
39+
name: string;
40+
fields: SimpleField[] | ComplexField[];
41+
suggesters: SearchSuggester[];
42+
};
43+
type Hotel = {
44+
HotelId: string;
45+
HotelName: string;
46+
Description: string;
47+
Description_fr: string;
48+
Category: string;
49+
Tags: string[];
50+
ParkingIncluded: string | boolean;
51+
LastRenovationDate: string;
52+
Rating: number;
53+
Address: Address;
54+
};
55+
type Address = {
56+
StreetAddress: string;
57+
City: string;
58+
StateProvince: string;
59+
PostalCode: string;
60+
};
61+
62+
// Get Incoming Data
63+
const indexDef: SearchIndex = indexDefinition as MyIndexDefinition;
64+
const hotels: Hotel[] = hotelData["value"];
65+
66+
async function createIndex(
67+
searchIndexClient: SearchIndexClient,
68+
indexName: string,
69+
searchIndex: SearchIndex,
70+
) {
71+
console.log(`Running Azure AI Search JavaScript quickstart...`);
72+
73+
// Delete the index if it already exists
74+
indexName
75+
? await searchIndexClient.deleteIndex(indexName)
76+
: console.log("Index doesn't exist.");
77+
78+
console.log("Creating index...");
79+
const newIndex: SearchIndex = await searchIndexClient.createIndex(searchIndex);
80+
81+
// Print the new index
82+
printSearchIndex(newIndex);
83+
}
84+
async function loadData(
85+
searchClient: any,
86+
hotels: any,
87+
) {
88+
console.log("Uploading documents...");
89+
90+
let indexDocumentsResult = await searchClient.mergeOrUploadDocuments(hotels);
91+
92+
console.log(JSON.stringify(indexDocumentsResult));
93+
94+
console.log(
95+
`Index operations succeeded: ${JSON.stringify(indexDocumentsResult.results[0].succeeded)}`,
96+
);
97+
}
98+
99+
async function searchAllReturnAllFields(
100+
searchClient: SearchClient<Hotel>,
101+
searchText: string,
102+
) {
103+
console.log("Query #1 - return everything:");
104+
105+
// Search Options
106+
// Without `select` property, all fields are returned
107+
const searchOptions = { includeTotalCount: true };
108+
109+
// Search
110+
const { count, results } = await searchClient.search(
111+
searchText,
112+
searchOptions,
113+
);
114+
115+
// Show results
116+
for await (const result of results) {
117+
console.log(`${JSON.stringify(result.document)}`);
118+
}
119+
120+
// Show results count
121+
console.log(`Result count: ${count}`);
122+
}
123+
async function searchAllSelectReturnedFields(
124+
searchClient: SearchClient<Hotel>,
125+
searchText: string,
126+
) {
127+
console.log("Query #2 - search everything:");
128+
129+
// Search Options
130+
// Narrow down the fields to return
131+
const selectFields: SearchFieldArray<Hotel> = [
132+
"HotelId",
133+
"HotelName",
134+
"Rating",
135+
];
136+
const searchOptions = { includeTotalCount: true, select: selectFields };
137+
138+
// Search
139+
const { count, results }: SearchDocumentsResult<Hotel> =
140+
await searchClient.search(searchText, searchOptions);
141+
142+
// Show results
143+
for await (const result of results) {
144+
console.log(`${JSON.stringify(result.document)}`);
145+
}
146+
147+
// Show results count
148+
console.log(`Result count: ${count}`);
149+
}
150+
async function searchWithFilterOrderByAndSelect(
151+
searchClient: SearchClient<Hotel>,
152+
searchText: string,
153+
filterText: string,
154+
) {
155+
console.log("Query #3 - Search with filter, orderBy, and select:");
156+
157+
// Search Options
158+
// Narrow down the fields to return
159+
const searchOptions = {
160+
filter: odata`Address/StateProvince eq ${filterText}`,
161+
orderBy: ["Rating desc"],
162+
select: ["HotelId", "HotelName", "Rating"] as const, // Select only these fields
163+
};
164+
type SelectedHotelFields = Pick<Hotel, "HotelId" | "HotelName" | "Rating">;
165+
166+
// Search
167+
const { count, results }: SearchDocumentsResult<SelectedHotelFields> =
168+
await searchClient.search(searchText, searchOptions);
169+
170+
// Show results
171+
for await (const result of results) {
172+
console.log(`${JSON.stringify(result.document)}`);
173+
}
174+
175+
// Show results count
176+
console.log(`Result count: ${count}`);
177+
}
178+
async function searchWithLimitedSearchFields(
179+
searchClient: SearchClient<Hotel>,
180+
searchText: string,
181+
) {
182+
console.log("Query #4 - Limit searchFields:");
183+
184+
// Search Options
185+
const searchOptions = {
186+
select: ["HotelId", "HotelName", "Rating"] as const, // Select only these fields
187+
searchFields: ["HotelName"] as const,
188+
};
189+
type SelectedHotelFields = Pick<Hotel, "HotelId" | "HotelName" | "Rating">;
190+
191+
// Search
192+
const { count, results }: SearchDocumentsResult<SelectedHotelFields> =
193+
await searchClient.search(searchText, searchOptions);
194+
195+
// Show results
196+
for await (const result of results) {
197+
console.log(`${JSON.stringify(result.document)}`);
198+
}
199+
200+
// Show results count
201+
console.log(`Result count: ${count}`);
202+
}
203+
async function searchWithFacets(
204+
searchClient: SearchClient<Hotel>,
205+
searchText: string,
206+
) {
207+
console.log("Query #5 - Use facets:");
208+
209+
// Search Options
210+
const searchOptions = {
211+
facets: ["Category"], //For aggregation queries
212+
select: ["HotelId", "HotelName", "Rating"] as const, // Select only these fields
213+
searchFields: ["HotelName"] as const,
214+
};
215+
type SelectedHotelFields = Pick<Hotel, "HotelName">;
216+
217+
// Search
218+
const { count, results }: SearchDocumentsResult<SelectedHotelFields> =
219+
await searchClient.search(searchText, searchOptions);
220+
221+
// Show results
222+
for await (const result of results) {
223+
console.log(`${JSON.stringify(result.document)}`);
224+
}
225+
226+
// Show results count
227+
console.log(`Result count: ${count}`);
228+
}
229+
async function lookupDocumentById(
230+
searchClient: SearchClient<Hotel>,
231+
key: string,
232+
) {
233+
console.log("Query #6 - Lookup document:");
234+
235+
// Get document by ID
236+
// Full document returned, destructured into id and name
237+
const { HotelId: id, HotelName: name }: Hotel =
238+
await searchClient.getDocument(key);
239+
240+
// Show results
241+
console.log(`HotelId: ${id}, HotelName: ${name}`);
242+
}
243+
244+
async function main(indexName: string, indexDef: SearchIndex, hotels: Hotel[]) {
245+
// Create a new SearchIndexClient
246+
const indexClient = new SearchIndexClient(
247+
endpoint,
248+
new AzureKeyCredential(apiKey),
249+
);
250+
251+
// Create the index
252+
await createIndex(indexClient, indexName, indexDef);
253+
254+
const searchClient = indexClient.getSearchClient(indexName);
255+
//const searchClient = new SearchClient(endpoint, indexName, new AzureKeyCredential(apiKey));
256+
257+
258+
// Load with data
259+
console.log("Loading data...", indexName);
260+
await loadData(searchClient, hotels);
261+
262+
wait(10000);
263+
264+
// Search index
265+
// await searchAllReturnAllFields(searchClient, "*");
266+
// await searchAllSelectReturnedFields(searchClient, "*");
267+
// await searchWithFilterOrderByAndSelect(searchClient, "wifi", "FL");
268+
// await searchWithLimitedSearchFields(searchClient, "sublime cliff");
269+
// await searchWithFacets(searchClient, "*");
270+
// await lookupDocumentById(searchClient, "3");
271+
}
272+
273+
main(indexDefinition?.name, indexDef, hotels).catch((err) => {
274+
console.error("The sample encountered an error:", err);
275+
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"value": [
3+
{
4+
"HotelId": "1",
5+
"HotelName": "Secret Point Motel",
6+
"Description": "The hotel is ideally located on the main commercial artery of the city in the heart of New York. A few minutes away is Time's Square and the historic centre of the city, as well as other places of interest that make New York one of America's most attractive and cosmopolitan cities.",
7+
"Description_fr": "L'hôtel est idéalement situé sur la principale artère commerciale de la ville en plein cœur de New York. A quelques minutes se trouve la place du temps et le centre historique de la ville, ainsi que d'autres lieux d'intérêt qui font de New York l'une des villes les plus attractives et cosmopolites de l'Amérique.",
8+
"Category": "Boutique",
9+
"Tags": ["pool", "air conditioning", "concierge"],
10+
"ParkingIncluded": false,
11+
"LastRenovationDate": "1970-01-18T00:00:00Z",
12+
"Rating": 3.6,
13+
"Address": {
14+
"StreetAddress": "677 5th Ave",
15+
"City": "New York",
16+
"StateProvince": "NY",
17+
"PostalCode": "10022"
18+
}
19+
},
20+
{
21+
"HotelId": "2",
22+
"HotelName": "Twin Dome Motel",
23+
"Description": "The hotel is situated in a nineteenth century plaza, which has been expanded and renovated to the highest architectural standards to create a modern, functional and first-class hotel in which art and unique historical elements coexist with the most modern comforts.",
24+
"Description_fr": "L'hôtel est situé dans une place du XIXe siècle, qui a été agrandie et rénovée aux plus hautes normes architecturales pour créer un hôtel moderne, fonctionnel et de première classe dans lequel l'art et les éléments historiques uniques coexistent avec le confort le plus moderne.",
25+
"Category": "Boutique",
26+
"Tags": ["pool", "free wifi", "concierge"],
27+
"ParkingIncluded": "false",
28+
"LastRenovationDate": "1979-02-18T00:00:00Z",
29+
"Rating": 3.6,
30+
"Address": {
31+
"StreetAddress": "140 University Town Center Dr",
32+
"City": "Sarasota",
33+
"StateProvince": "FL",
34+
"PostalCode": "34243"
35+
}
36+
},
37+
{
38+
"HotelId": "3",
39+
"HotelName": "Triple Landscape Hotel",
40+
"Description": "The Hotel stands out for its gastronomic excellence under the management of William Dough, who advises on and oversees all of the Hotel’s restaurant services.",
41+
"Description_fr": "L'hôtel est situé dans une place du XIXe siècle, qui a été agrandie et rénovée aux plus hautes normes architecturales pour créer un hôtel moderne, fonctionnel et de première classe dans lequel l'art et les éléments historiques uniques coexistent avec le confort le plus moderne.",
42+
"Category": "Resort and Spa",
43+
"Tags": ["air conditioning", "bar", "continental breakfast"],
44+
"ParkingIncluded": "true",
45+
"LastRenovationDate": "2015-09-20T00:00:00Z",
46+
"Rating": 4.8,
47+
"Address": {
48+
"StreetAddress": "3393 Peachtree Rd",
49+
"City": "Atlanta",
50+
"StateProvince": "GA",
51+
"PostalCode": "30326"
52+
}
53+
},
54+
{
55+
"HotelId": "4",
56+
"HotelName": "Sublime Cliff Hotel",
57+
"Description": "Sublime Cliff Hotel is located in the heart of the historic center of Sublime in an extremely vibrant and lively area within short walking distance to the sites and landmarks of the city and is surrounded by the extraordinary beauty of churches, buildings, shops and monuments. Sublime Cliff is part of a lovingly restored 1800 palace.",
58+
"Description_fr": "Le sublime Cliff Hotel est situé au coeur du centre historique de sublime dans un quartier extrêmement animé et vivant, à courte distance de marche des sites et monuments de la ville et est entouré par l'extraordinaire beauté des églises, des bâtiments, des commerces et Monuments. Sublime Cliff fait partie d'un Palace 1800 restauré avec amour.",
59+
"Category": "Boutique",
60+
"Tags": ["concierge", "view", "24-hour front desk service"],
61+
"ParkingIncluded": true,
62+
"LastRenovationDate": "1960-02-06T00:00:00Z",
63+
"Rating": 4.6,
64+
"Address": {
65+
"StreetAddress": "7400 San Pedro Ave",
66+
"City": "San Antonio",
67+
"StateProvince": "TX",
68+
"PostalCode": "78216"
69+
}
70+
}
71+
]
72+
}

0 commit comments

Comments
 (0)