이 가이드에서는 순전히 키워드가 아닌 검색어 의미를 기반으로 관련 콘텐츠를 찾는 검색 기법인 기기 내 시맨틱 검색을 구현하는 방법을 설명합니다. 시맨틱 검색을 사용하면 커스텀 동의어 테이블이나 태그 분류를 수동으로 빌드하고 유지관리할 필요가 없습니다. 기기 내 시맨틱 검색을 구현하려면 다음 라이브러리를 사용하세요.
- ML Kit SDK, 특히 임베딩 API를 사용하여 벡터 임베딩 생성
- AndroidX AppSearch: 벡터 임베딩을 저장하고 코사인 거리 측정항목을 사용하여 구조화된 데이터를 검색하여 시맨틱으로 콘텐츠를 찾습니다.
기본 요건
기기 내 시맨틱 검색을 사용하려면 다음 기기 및 계정 요구사항을 충족해야 합니다.
- 기기가 Android AICore를 지원합니다. AICore 지원 기기는 대부분 NPU 가속 모델을 사용하며, 나머지 지원 기기는 CPU 모델(XNNPACK 가속 사용)로 대체됩니다.
- 기기의 Google 계정이 AICore 개발자 프리뷰 프로그램에 참여 중입니다.
- 기기에 최신 버전의 AICore 개발자 프리뷰 APK가 있습니다. 기기에서 Android AICore Play 스토어 페이지를 열고 업데이트를 탭하여 최신 버전의 APK를 설치합니다. 또는 기기 설정으로 이동하여 시스템 서비스 또는 Google 서비스를 검색하고 Android AICore (베타)를 탭한 다음 업데이트가 있는 경우 업데이트를 탭합니다.
- 기기에 최신 버전의 프라이빗 컴퓨트 서비스 앱이 있습니다. 기기에서 프라이빗 컴퓨트 서비스 Play 스토어 페이지를 열고 업데이트를 탭하여 최신 버전의 앱을 설치합니다. 또는 Play 스토어 > 앱 및 기기 관리 > 관리 > 프라이빗 컴퓨트 서비스로 이동하여 업데이트가 있는 경우 업데이트를 탭합니다.
지원되는 모델 및 구성
ML Kit Embedding API는 Android AICore에서 관리하는 Google의 EmbeddingGemma 모델 제품군을 사용합니다. 구체적으로 Embedding API는 EmbeddingGemma 2(embedding_gemma_v2)를 사용하여 텍스트 및 이미지 입력의 임베딩을 생성합니다.
입력 사양 및 제한사항은 다음과 같습니다.
| 입력 속성 | EmbeddingGemma 2 | 사용 참고사항 |
|---|---|---|
| 지원되는 모달리티 | 텍스트 및 이미지 (Modality.TEXT, Modality.IMAGE) |
이미지 입력이 지원되는지 확인하려면 런타임에 model.getMetadata()?.supportedModalities.contains(Modality.IMAGE)가 true를 반환하는지 확인합니다. |
| 토큰 한도 | 토큰 256개 | model.getTokenLimit() 메서드를 사용하여 검사합니다. 각 이미지는 70개의 토큰으로 인코딩되며, 함께 제공되는 텍스트에는 186개의 토큰이 남습니다. |
| 이미지 해상도 | 짧은 쪽이 최대 768픽셀 | ImagePart 클래스의 생성자는 Bitmap, Uri 또는 ByteArray 인수 유형을 허용합니다. 짧은 쪽이 768픽셀보다 큰 이미지는 SDK에서 가로세로 비율을 유지하면서 자동으로 축소됩니다. 최적의 성능과 메모리 사용을 위해 극단적인 가로세로 비율은 피하세요. |
출력 벡터 차원은 768입니다 (32비트 부동 소수점의 FloatArray 객체). model.getMetadata()?.dimension 메서드를 사용하여 런타임에 출력 벡터 차원을 검사할 수 있습니다.
시작하기
시맨틱 검색을 사용하도록 프로젝트를 구성하려면 모듈 수준 build.gradle.kts (또는 build.gradle) 파일에 다음 종속 항목을 추가합니다.
Kotlin
dependencies {
// ...
implementation("com.google.mlkit:genai-embedding:1.0.0-alpha1")
implementation("androidx.appsearch:appsearch:1.2.0-alpha01")
implementation("androidx.appsearch:appsearch-local-storage:1.2.0-alpha01")
implementation("androidx.concurrent:concurrent-futures-ktx:1.3.0")
// ...
}
Groovy
dependencies {
// ...
implementation 'com.google.mlkit:genai-embedding:1.0.0-alpha1'
implementation 'androidx.appsearch:appsearch:1.2.0-alpha01'
implementation 'androidx.appsearch:appsearch-local-storage:1.2.0-alpha01'
implementation 'androidx.concurrent:concurrent-futures-ktx:1.3.0'
// ...
}
사용 가능한 모델을 가져오려면 EmbeddingGeneration.getEmbeddingModels(EmbeddingOptions()) 메서드를 사용합니다.
AppSearch 스키마 정의
의미론적 유사성 검색을 사용 설정하려면 벡터 속성이 유사성 색인 생성에 명시적으로 구성된 AppSearch 스키마를 정의해야 합니다.
// Define the document schema
val documentSchema = AppSearchSchema.Builder("my_schema_type")
.addProperty(
AppSearchSchema.StringPropertyConfig.Builder("text")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.build()
)
.addProperty(
AppSearchSchema.StringPropertyConfig.Builder("imageUri")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
.build()
)
.addProperty(
AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setIndexingType(
AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
.setQuantizationType(
AppSearchSchema.EmbeddingPropertyConfig.QUANTIZATION_TYPE_8_BIT)
.build()
)
.build()
문서 콘텐츠 색인 생성
문서 콘텐츠의 색인을 생성하려면 각 Content 객체에 대해 벡터 임베딩을 생성하고, 이를 GenericDocument 인스턴스로 래핑한 후 AppSearch 데이터베이스에 저장해야 합니다.
EmbeddingVector 객체를 구성할 때 활성 modelSignature 값도 전달해야 합니다. AppSearch는 이 modelSignature 문자열을 사용하여 호환되지 않는 검색어를 방지합니다.
suspend fun syncDatabase(documents: List<Pair<String, Uri?>>) {
val modelSignature = model?.getModelSignature() ?: return
val docsWithEmbedding = mutableListOf<GenericDocument>()
for ((index, item) in documents.withIndex()) {
val (content, imageUri) = item
// 1. Generate embedding (text and/or image)
val parts = mutableListOf<Part>()
if (content.isNotBlank()) {
parts.add(TextPart(content))
}
var imagePart: ImagePart? = null
val vector = try {
if (imageUri != null && isImageSupported) {
imagePart = ImagePart(imageUri)
parts.add(imagePart)
}
if (parts.isEmpty()) continue
val request = EmbeddingRequest(
Content(parts)
)
model?.generateEmbedding(request)?.embedding?.vector
?: continue
} finally {
imagePart?.close()
}
// 2. Build the AppSearch document
val docBuilder = GenericDocument.Builder<GenericDocument.Builder<*>>(
"my_namespace",
"doc_$index",
"my_schema_type"
)
.setPropertyString("text", content)
// Associate the vector with the signature that generated it
.setPropertyEmbedding("embedding", EmbeddingVector(vector, modelSignature))
if (imageUri != null) {
docBuilder.setPropertyString("imageUri", imageUri.toString())
}
docsWithEmbedding.add(docBuilder.build())
}
// 3. Clear the old database by force-overriding the schema
val clearRequest = SetSchemaRequest.Builder().setForceOverride(true).build()
session?.setSchemaAsync(clearRequest)?.await()
val schemaRequest = SetSchemaRequest.Builder().addSchemas(documentSchema).build()
session?.setSchemaAsync(schemaRequest)?.await()
// 4. Index documents
val putRequest = PutDocumentsRequest.Builder().addGenericDocuments(docsWithEmbedding).build()
session?.putAsync(putRequest)?.await()
}
시맨틱 검색 실행
시맨틱 쿼리를 수행하려면 검색어 (텍스트 또는 이미지)를 임베딩 벡터로 변환하고, 벡터 검색 매개변수를 구성하고, AppSearch 데이터베이스를 쿼리합니다.
권장사항:
- 기본 측정항목을
SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE상수로 설정합니다. "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))"수식으로 결과를 순위 지정합니다.- 논리적
"semanticSearch(getEmbeddingParameter(0), $threshold)"문자열을 사용하여 유사성이 임곗값(일반적으로 0.5 이상)을 충족하는 결과를 필터링합니다.
suspend fun search(
queryText: String,
imageUri: Uri? = null,
threshold: Double = 0.5
): List<Triple<String, Uri?, Double>> {
val modelSignature = model?.getModelSignature() ?: return emptyList()
// 1. Generate query embedding (text and/or image)
val parts = mutableListOf<Part>()
if (queryText.isNotBlank()) {
parts.add(TextPart(queryText))
}
var imagePart: ImagePart? = null
val queryVector = try {
if (imageUri != null && isImageSupported) {
imagePart = ImagePart(imageUri)
parts.add(imagePart)
}
if (parts.isEmpty()) return emptyList()
val request = EmbeddingRequest(
content = Content(parts)
)
model?.generateEmbedding(request)?.embedding?.vector
?: return emptyList()
} finally {
imagePart?.close()
}
// 2. Configure search specifications
val searchSpec = SearchSpec.Builder()
.addFilterSchemas("my_schema_type")
.setDefaultEmbeddingSearchMetricType(SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
.setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
.setListFilterQueryLanguageEnabled(true)
.setResultCountPerPage(5)
// Add the query vector as parameter 0
.addEmbeddingParameters(EmbeddingVector(queryVector, modelSignature))
.build()
// 3. Execute search
val queryExpression = "semanticSearch(getEmbeddingParameter(0), $threshold)"
val results = session?.search(queryExpression, searchSpec) ?: return emptyList()
return try {
val page = results.nextPageAsync.await()
page.map {
val text = it.genericDocument.getPropertyString("text") ?: ""
val matchedImageUri = it.genericDocument.getPropertyString("imageUri")?.let(Uri::parse)
// rankingSignal contains the computed similarity score
Triple(text, matchedImageUri, it.rankingSignal)
}
} finally {
results.close()
}
}
하이브리드 검색 색인 사용
최상의 사용자 환경을 위해 시맨틱 검색과 키워드 검색을 모두 포함하는 하이브리드 전략을 사용하는 것이 좋습니다. 사용자가 검색창에 적극적으로 입력하는 경우 키워드 검색을 사용하여 즉각적인 자동 완성 추천을 표시합니다. 완성된 검색어를 입력하거나 단어 나누기에서도 시맨틱 검색을 사용하여 가장 적합한 개념적 일치를 읽고 이해하고 가져옵니다.
벡터 데이터베이스 색인 다시 생성
서로 다른 모델 버전에서 생성된 임베딩은 호환되지 않는 벡터 공간에 상주하며 직접 비교할 수 없습니다. 임베딩 모델을 업데이트할 때마다 AppSearch 데이터베이스의 색인을 다시 생성해야 합니다.
사용 중인 모델이 지원 중단 기간에 들어가면 즉시 임베딩 색인 생성을 다시 시작하는 것이 좋습니다. Embedding API를 사용하여 모델의 지원 중단 일정을 확인할 수 있습니다. model.getMetadata() 메서드를 호출하여 EmbeddingMetadata 객체를 가져오고 다음 수명 주기 날짜 속성(UTC에서 LocalDate 객체로 반환됨)을 확인합니다.
deprecationDate값은 모델 지원 중단 기간의 시작을 표시합니다. 모델이 계속 작동하는 동안 색인 재생성을 시작하고 공식 종료 전에 색인 재생성을 완료해야 합니다.shutdownDate값은 모델이 사용 중단되고 더 이상 제공되지 않는 시점입니다. 이 날짜 이후에는 추론을 위해 모델에 액세스할 수 없으며 이 모델의 기존 임베딩은 사용 가능한 최신 모델로 강제 재색인 생성됩니다.
또한 ML Kit 출시 노트 및 개발자 문서 업데이트를 통해 예정된 모델 지원 중단, 마이그레이션 타임라인, 목표 종료 날짜를 안내합니다.
권장사항
우수한 사용자 환경을 제공하려면 벡터 임베딩을 Android 앱에 통합할 때 다음 권장사항을 따르세요.
- 색인 생성 워크로드 예약: 항상 WorkManager 라이브러리를 사용하여 백그라운드 색인 생성 워크로드를 예약합니다. 기기가 충전 중이고 유휴 상태일 때만 실행되도록 백그라운드 작업을 구성합니다. ML Kit는 LLM API와 마찬가지로 모델 사용량에 할당량 한도를 적용하며, 기기가 충전 중일 때는 한도가 더 완화됩니다. 개발자 프리뷰 모드에서는 테스트 목적으로 할당량 제한을 우회할 수 있습니다.
- 스토리지 및 쿼리 최적화: 양자화 및 차원 자르기를 사용하여 데이터베이스 공간과 쿼리 지연 시간을 최적화합니다.
- AppSearch 스키마 정의에 표시된 대로
QUANTIZATION_TYPE_8_BIT상수를 사용하도록AppSearchSchema스키마를 구성합니다. 이렇게 하면 각 좌표의 정밀도를 8비트 정수로 축소하여 품질에 미치는 영향을 최소화하면서 메모리 사용량을 줄일 수 있습니다. - EmbeddingGemma 2 모델에서 지원하는 마트료시카 표현 학습 (MRL)을 사용하여 임베딩 벡터를 자릅니다. 자세한 내용은 HuggingFace의 EmbeddingGemma 2를 참고하세요.
- AppSearch 스키마 정의에 표시된 대로
- 서명 일관성: 일치하지 않는 서명을 사용하여 벡터 공간을 쿼리하지 마세요. 임베딩 모델 버전을 업그레이드하는 경우 AppSearch 데이터베이스를 완전 삭제하고 새 모델 서명을 사용하여 모든 문서의 임베딩을 다시 생성해야 합니다.
- 안전 및 콘텐츠 검토: 앱에서 콘텐츠 검토 및 안전 필터링을 구현할 책임은 개발자에게 있습니다. 생성형 텍스트 또는 이미지 API와 달리 ML Kit Embedding API에는 안전 필터가 내장되어 있지 않습니다. 모델은 새로운 텍스트나 미디어를 생성하는 대신 호출자가 제공한 입력의 숫자 벡터 표현(
FloatArray좌표)을 출력하므로 숫자 벡터는 본질적으로 생성형 콘텐츠 안전 위험을 초래하지 않습니다. 기기 내 안전 필터링을 생략하면 높은 처리량의 문서 색인 생성 및 실시간 쿼리 중에 CPU 경합을 방지하여 짧은 지연 시간의 색인 생성 및 검색 성능을 유지할 수 있습니다. 안전성을 개선하려면 다음 가이드라인을 따르세요.- 신뢰할 수 없는 입력 및 색인이 생성된 콘텐츠 검토: 앱에서 신뢰할 수 없는 사용자 제작 콘텐츠 또는 외부 데이터를 색인 생성하거나 검색하는 경우 소스 텍스트 또는 이미지에 콘텐츠 검토 및 정책 필터를 적용한 후 색인을 생성하거나 검색 결과를 표시합니다.
- 다운스트림 생성형 AI 파이프라인 보호: 검색된 문서 또는 구절이 다운스트림 생성형 AI 모델에 컨텍스트로 전달되는 경우 해당 다운스트림 모델과 사용자 대상 출력에서 안전 필터와 콘텐츠 정책이 사용 설정되어 있는지 확인합니다.