앱 콘텐츠 검색을 사용하여 앱 내 콘텐츠의 의미 체계 인덱스 만들기 이를 통해 사용자는 키워드가 아닌 의미에 따라 정보를 찾을 수 있습니다. 또한 인덱스를 사용하여 보다 개인화되고 상황에 맞는 결과를 위해 도메인별 지식을 갖춘 AI 도우미를 향상시킬 수 있습니다.
특히 AppContentIndexer API를 사용하여 다음을 수행합니다.
- 앱에서 콘텐츠의 인덱스 만들기 또는 열기
- 인덱스에 텍스트 문자열을 추가한 다음 쿼리를 실행합니다.
- 긴 텍스트 문자열 복잡성 관리
- 이미지 데이터를 인덱싱한 다음 관련 이미지 검색
- RAG(Retrieval-Augmented 생성하기) 시나리오 사용
- 백그라운드 스레드에서 AppContentIndexer 사용
- 리소스를 해제하는 데 더 이상 사용되지 않는 경우 AppContentIndexer를 닫습니다.
필수 조건
Windows AI API 하드웨어 요구 사항 및 Windows AI API를 사용하여 앱을 성공적으로 빌드하도록 디바이스를 구성하는 방법에 대해 알아보려면 Windows AI API를 사용하여 앱 빌드 시작을 참조하세요.
패키지 ID 요구 사항
AppContentIndexer를 사용하는 앱에는 패키지 ID가 있어야 하며 패키지된 앱(외부 위치 포함)에서만 사용할 수 있습니다. 의미 체계 인덱싱 및 OCR(텍스트 인식)을 사용하도록 설정하려면 앱도 기능을 선언systemaimodels해야 합니다.
앱에서 콘텐츠의 인덱스 만들기 또는 열기
앱에서 콘텐츠의 의미 체계 인덱싱을 만들려면 먼저 앱이 콘텐츠를 효율적으로 저장하고 검색하는 데 사용할 수 있는 검색 가능한 구조를 설정해야 합니다. 이 인덱스가 앱 콘텐츠에 대한 로컬 의미 체계 및 어휘 검색 엔진 역할을 합니다.
AppContentIndexer API를 사용하려면 먼저 지정된 인덱스 이름을 사용하여 호출 GetOrCreateIndex 합니다. 현재 앱 ID 및 사용자에 대해 해당 이름의 인덱스가 이미 있는 경우 해당 인덱스가 열립니다. 그렇지 않으면 새 항목이 만들어집니다.
public void SimpleGetOrCreateIndexSample()
{
GetOrCreateIndexResult result = AppContentIndexer.GetOrCreateIndex("myindex");
if (!result.Succeeded)
{
throw new InvalidOperationException($"Failed to open index. Status = '{result.Status}', Error = '{result.ExtendedError}'");
}
// If result.Succeeded is true, result.Status will either be CreatedNew or OpenedExisting
if (result.Status == GetOrCreateIndexStatus.CreatedNew)
{
Console.WriteLine("Created a new index");
}
else if(result.Status == GetOrCreateIndexStatus.OpenedExisting)
{
Console.WriteLine("Opened an existing index");
}
using AppContentIndexer indexer = result.Indexer;
// Use indexer...
}
이 샘플에서는 인덱스 열기에 대한 오류 사례를 처리하는 오류를 보여줍니다. 간단히 하기 위해 이 문서의 다른 샘플은 오류 처리를 표시하지 않을 수 있습니다.
인덱스에 텍스트 문자열을 추가한 다음 쿼리를 실행합니다.
이 샘플에서는 앱용으로 만든 인덱스에 일부 텍스트 문자열을 추가한 다음 해당 인덱스에 대해 쿼리를 실행하여 관련 정보를 검색하는 방법을 보여 줍니다.
// This is some text data that we want to add to the index:
Dictionary<string, string> simpleTextData = new Dictionary<string, string>
{
{"item1", "Here is some information about Cats: Cats are cute and fluffy. Young cats are very playful." },
{"item2", "Dogs are loyal and affectionate animals known for their companionship, intelligence, and diverse breeds." },
{"item3", "Fish are aquatic creatures that breathe through gills and come in a vast variety of shapes, sizes, and colors." },
{"item4", "Broccoli is a nutritious green vegetable rich in vitamins, fiber, and antioxidants." },
{"item5", "Computers are powerful electronic devices that process information, perform calculations, and enable communication worldwide." },
{"item6", "Music is a universal language that expresses emotions, tells stories, and connects people through rhythm and melody." },
};
public void SimpleTextIndexingSample()
{
AppContentIndexer indexer = GetIndexerForApp();
// Add some text data to the index:
foreach (var item in simpleTextData)
{
IndexableAppContent textContent = AppManagedIndexableAppContent.CreateFromString(item.Key, item.Value);
indexer.AddOrUpdate(textContent);
}
}
public void SimpleTextQueryingSample()
{
AppContentIndexer indexer = GetIndexerForApp();
// We search the index using a semantic query:
AppIndexTextQuery queryCursor = indexer.CreateTextQuery("Facts about kittens.");
IReadOnlyList<TextQueryMatch> textMatches = queryCursor.GetNextMatches(5);
// Nothing in the index exactly matches what we queried but item1 is similar to the query so we expect
// that to be the first match.
foreach (var match in textMatches)
{
Console.WriteLine(match.ContentId);
if (match.ContentKind == QueryMatchContentKind.AppManagedText)
{
AppManagedTextQueryMatch textResult = (AppManagedTextQueryMatch)match;
// Only part of the original string may match the query. So we can use TextOffset and TextLength to extract the match.
// In this example, we might imagine that the substring "Cats are cute and fluffy" from "item1" is the top match for the query.
string matchingData = simpleTextData[match.ContentId];
string matchingString = matchingData.Substring(textResult.TextOffset, textResult.TextLength);
Console.WriteLine(matchingString);
}
}
}
QueryMatch에는 ContentId 및 TextOffset/TextLength만 포함되며, 일치하는 텍스트 자체는 포함되지 않습니다. 앱 개발자는 원래 텍스트를 참조해야 합니다. 쿼리 결과는 관련성별로 정렬되며 상위 결과가 가장 관련성이 높습니다. 인덱싱은 비동기적으로 발생하므로 부분 데이터에서 쿼리가 실행될 수 있습니다. 아래에 설명된 대로 인덱싱 상태를 확인할 수 있습니다.
긴 텍스트 문자열 복잡성 관리
이 샘플은 앱 개발자가 모델 처리를 위해 텍스트 콘텐츠를 더 작은 섹션으로 나눌 필요가 없음을 보여 줍니다. AppContentIndexer는 이러한 복잡성 측면을 관리합니다.
Dictionary<string, string> textFiles = new Dictionary<string, string>
{
{"file1", "File1.txt" },
{"file2", "File2.txt" },
{"file3", "File3.txt" },
};
public void TextIndexingSample2()
{
AppContentIndexer indexer = GetIndexerForApp();
var folderPath = Windows.ApplicationModel.Package.Current.InstalledLocation.Path;
// Add some text data to the index:
foreach (var item in textFiles)
{
string contentId = item.Key;
string filename = item.Value;
// Note that the text here can be arbitrarily large. The AppContentIndexer will take care of chunking the text
// in a way that works effectively with the underlying model. We do not require the app author to break the text
// down into small pieces.
string text = File.ReadAllText(Path.Combine(folderPath, filename));
IndexableAppContent textContent = AppManagedIndexableAppContent.CreateFromString(contentId, text);
indexer.AddOrUpdate(textContent);
}
}
public void TextIndexingSample2_RunQuery()
{
AppContentIndexer indexer = GetIndexerForApp();
var folderPath = Windows.ApplicationModel.Package.Current.InstalledLocation.Path;
// Search the index
AppIndexTextQuery query = indexer.CreateTextQuery("Facts about kittens.");
IReadOnlyList<TextQueryMatch> textMatches = query.GetNextMatches(5);
if (textMatches != null)
{
foreach (var match in textMatches)
{
Console.WriteLine(match.ContentId);
if (match is AppManagedTextQueryMatch textResult)
{
// We load the content of the file that contains the match:
string matchingFilename = textFiles[match.ContentId];
string fileContent = File.ReadAllText(Path.Combine(folderPath, matchingFilename));
// Find the substring within the loaded text that contains the match:
string matchingString = fileContent.Substring(textResult.TextOffset, textResult.TextLength);
Console.WriteLine(matchingString);
}
}
}
}
텍스트 데이터는 파일에서 원본이 되지만 콘텐츠만 인덱싱되고 파일 자체는 인덱싱되지 않습니다. AppContentIndexer 는 원래 파일에 대한 지식이 없으며 업데이트를 모니터링하지 않습니다. 파일 콘텐츠가 변경되면 앱은 인덱스 수동으로 업데이트해야 합니다.
이미지 데이터를 인덱싱한 다음 관련 이미지 검색
이 샘플에서는 이미지 데이터를 인 SoftwareBitmaps 덱싱한 다음 텍스트 쿼리를 사용하여 관련 이미지를 검색하는 방법을 보여 줍니다.
// We load the image data from a set of known files and send that image data to the indexer.
// The image data does not need to come from files on disk, it can come from anywhere.
Dictionary<string, string> imageFilesToIndex = new Dictionary<string, string>
{
{"item1", "Cat.jpg" },
{"item2", "Dog.jpg" },
{"item3", "Fish.jpg" },
{"item4", "Broccoli.jpg" },
{"item5", "Computer.jpg" },
{"item6", "Music.jpg" },
};
public void SimpleImageIndexingSample()
{
AppContentIndexer indexer = GetIndexerForApp();
// Add some image data to the index.
foreach (var item in imageFilesToIndex)
{
var file = item.Value;
var softwareBitmap = Helpers.GetSoftwareBitmapFromFile(file);
IndexableAppContent imageContent = AppManagedIndexableAppContent.CreateFromBitmap(item.Key, softwareBitmap);
indexer.AddOrUpdate(imageContent);
}
}
public void SimpleImageIndexingSample_RunQuery()
{
AppContentIndexer indexer = GetIndexerForApp();
// We query the index for some data to match our text query.
AppIndexImageQuery query = indexer.CreateImageQuery("cute pictures of kittens");
IReadOnlyList<ImageQueryMatch> imageMatches = query.GetNextMatches(5);
// One of the images that we indexed was a photo of a cat. We expect this to be the first match to match the query.
foreach (var match in imageMatches)
{
Console.WriteLine(match.ContentId);
if (match.ContentKind == QueryMatchContentKind.AppManagedImage)
{
AppManagedImageQueryMatch imageResult = (AppManagedImageQueryMatch)match;
var matchingFileName = imageFilesToIndex[match.ContentId];
// It might be that the match is at a particular region in the image. The result includes
// the subregion of the image that includes the match.
Console.WriteLine($"Matching file: '{matchingFileName}' at location {imageResult.Subregion}");
}
}
}
RAG(Retrieval-Augmented 생성하기) 시나리오 사용
RAG(Retrieval-Augmented Generation)는 언어 모델로 사용자 쿼리를 보강하여 응답을 생성하는 데 사용될 수 있는 추가 관련 데이터를 포함합니다. 사용자의 쿼리는 인덱스의 관련 정보를 식별하는 의미 체계 검색에 대한 입력 역할을 합니다. 그런 다음 의미 체계 검색의 결과 데이터를 언어 모델에 지정된 프롬프트에 통합하여 보다 정확하고 컨텍스트 인식 응답을 생성할 수 있습니다.
이 샘플에서는 APPContentIndexer API 를 LLM과 함께 사용하여 앱 사용자의 검색 쿼리에 컨텍스트 데이터를 추가하는 방법을 보여 줍니다. 샘플은 제네릭이며 LLM이 지정되지 않으며, 예제에서는 생성된 인덱스에 저장된 로컬 데이터만 쿼리합니다(인터넷에 대한 외부 호출 없음). 이 샘플 Helpers.GetUserPrompt()Helpers.GetResponseFromChatAgent() 에서는 실제 함수가 아니며 예제를 제공하는 데만 사용됩니다.
AppContentIndexer API를 사용하여 RAG 시나리오를 사용하도록 설정하려면 다음 예제를 따를 수 있습니다.
public void SimpleRAGScenario()
{
AppContentIndexer indexer = GetIndexerForApp();
// These are some text files that had previously been added to the index.
// The key is the contentId of the item.
Dictionary<string, string> data = new Dictionary<string, string>
{
{"file1", "File1.txt" },
{"file2", "File2.txt" },
{"file3", "File3.txt" },
};
string userPrompt = Helpers.GetUserPrompt();
// We execute a query against the index using the user's prompt string as the query text.
AppIndexTextQuery query = indexer.CreateTextQuery(userPrompt);
IReadOnlyList<TextQueryMatch> textMatches = query.GetNextMatches(5);
StringBuilder promptStringBuilder = new StringBuilder();
promptStringBuilder.AppendLine("Please refer to the following pieces of information when responding to the user's prompt:");
// For each of the matches found, we include the relevant snippets of the text files in the augmented query that we send to the language model
foreach (var match in textMatches)
{
if (match is AppManagedTextQueryMatch textResult)
{
// We load the content of the file that contains the match:
string matchingFilename = data[match.ContentId];
string fileContent = File.ReadAllText(matchingFilename);
// Find the substring within the loaded text that contains the match:
string matchingString = fileContent.Substring(textResult.TextOffset, textResult.TextLength);
promptStringBuilder.AppendLine(matchingString);
promptStringBuilder.AppendLine();
}
}
promptStringBuilder.AppendLine("Please provide a response to the following user prompt:");
promptStringBuilder.AppendLine(userPrompt);
var response = Helpers.GetResponseFromChatAgent(promptStringBuilder.ToString());
Console.WriteLine(response);
}
백그라운드 스레드에서 AppContentIndexer 사용
AppContentIndexer 인스턴스는 특정 스레드와 연결되지 않습니다. 스레드 간에 작동할 수 있는 Agile 개체입니다. AppContentIndexer 및 관련 형식의 특정 메서드에는 상당한 처리 시간이 필요할 수 있습니다. 따라서 애플리케이션의 UI 스레드에서 직접 AppContentIndexer API를 호출하지 않고 백그라운드 스레드를 사용하는 것이 좋습니다.
리소스를 해제하는 데 더 이상 사용되지 않는 경우 AppContentIndexer를 닫습니다.
AppContentIndexer 는 인터페이스를 IClosable 구현하여 수명을 확인합니다. 애플리케이션이 더 이상 사용되지 않는 경우 인덱서를 닫아야 합니다. 이를 통해 AppContentIndexer는 기본 리소스를 해제할 수 있습니다.
public void IndexerDisposeSample()
{
var indexer = AppContentIndexer.GetOrCreateIndex("myindex").Indexer;
// use indexer
indexer.Dispose();
// after this point, it would be an error to try to use indexer since it is now Closed.
}
C# 코드에서 인터페이스는 IClosable .로 IDisposable프로젝션됩니다. C# 코드는 usingAppContentIndexer 인스턴스에 대한 패턴을 사용할 수 있습니다.
public void IndexerUsingSample()
{
using var indexer = AppContentIndexer.GetOrCreateIndex("myindex").Indexer;
// use indexer
//indexer.Dispose() is automatically called
}
앱에서 동일한 인덱스가 여러 번 열려 있는 경우 각 인스턴스에서 호출 Close 해야 합니다.
인덱스를 열고 닫는 작업은 비용이 많이 들기 때문에 애플리케이션에서 이러한 작업을 최소화해야 합니다. 예를 들어 애플리케이션은 애플리케이션에 대한 AppContentIndexer 의 단일 인스턴스를 저장하고 수행해야 하는 각 작업에 대한 인덱스를 지속적으로 열고 닫는 대신 애플리케이션의 수명 동안 해당 인스턴스를 사용할 수 있습니다.