1 /* 2 * Copyright 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.appsearch.external.localstorage.converter; 18 19 import android.app.appsearch.AppSearchBlobHandle; 20 import android.app.appsearch.AppSearchSchema; 21 import android.app.appsearch.EmbeddingVector; 22 import android.app.appsearch.GenericDocument; 23 import android.app.appsearch.exceptions.AppSearchException; 24 25 import com.android.appsearch.flags.Flags; 26 import com.android.server.appsearch.external.localstorage.AppSearchConfig; 27 import com.android.server.appsearch.external.localstorage.SchemaCache; 28 29 import com.google.android.icing.proto.DocumentProto; 30 import com.google.android.icing.proto.DocumentProtoOrBuilder; 31 import com.google.android.icing.proto.PropertyProto; 32 import com.google.android.icing.proto.SchemaTypeConfigProto; 33 import com.google.protobuf.ByteString; 34 35 import org.jspecify.annotations.NonNull; 36 37 import java.util.ArrayList; 38 import java.util.Collections; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.Objects; 42 43 /** 44 * Translates a {@link GenericDocument} into a {@link DocumentProto}. 45 * 46 * @hide 47 */ 48 public final class GenericDocumentToProtoConverter { 49 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 50 private static final long[] EMPTY_LONG_ARRAY = new long[0]; 51 private static final double[] EMPTY_DOUBLE_ARRAY = new double[0]; 52 private static final boolean[] EMPTY_BOOLEAN_ARRAY = new boolean[0]; 53 private static final byte[][] EMPTY_BYTES_ARRAY = new byte[0][0]; 54 private static final GenericDocument[] EMPTY_DOCUMENT_ARRAY = new GenericDocument[0]; 55 private static final EmbeddingVector[] EMPTY_EMBEDDING_ARRAY = new EmbeddingVector[0]; 56 GenericDocumentToProtoConverter()57 private GenericDocumentToProtoConverter() {} 58 59 /** Converts a {@link GenericDocument} into a {@link DocumentProto}. */ 60 @SuppressWarnings("unchecked") toDocumentProto(@onNull GenericDocument document)61 public static @NonNull DocumentProto toDocumentProto(@NonNull GenericDocument document) { 62 Objects.requireNonNull(document); 63 DocumentProto.Builder mProtoBuilder = DocumentProto.newBuilder(); 64 mProtoBuilder 65 .setUri(document.getId()) 66 .setSchema(document.getSchemaType()) 67 .setNamespace(document.getNamespace()) 68 .setScore(document.getScore()) 69 .setTtlMs(document.getTtlMillis()) 70 .setCreationTimestampMs(document.getCreationTimestampMillis()); 71 ArrayList<String> keys = new ArrayList<>(document.getPropertyNames()); 72 Collections.sort(keys); 73 for (int i = 0; i < keys.size(); i++) { 74 String name = keys.get(i); 75 PropertyProto.Builder propertyProto = PropertyProto.newBuilder().setName(name); 76 Object property = document.getProperty(name); 77 if (property instanceof String[]) { 78 String[] stringValues = (String[]) property; 79 for (int j = 0; j < stringValues.length; j++) { 80 propertyProto.addStringValues(stringValues[j]); 81 } 82 } else if (property instanceof long[]) { 83 long[] longValues = (long[]) property; 84 for (int j = 0; j < longValues.length; j++) { 85 propertyProto.addInt64Values(longValues[j]); 86 } 87 } else if (property instanceof double[]) { 88 double[] doubleValues = (double[]) property; 89 for (int j = 0; j < doubleValues.length; j++) { 90 propertyProto.addDoubleValues(doubleValues[j]); 91 } 92 } else if (property instanceof boolean[]) { 93 boolean[] booleanValues = (boolean[]) property; 94 for (int j = 0; j < booleanValues.length; j++) { 95 propertyProto.addBooleanValues(booleanValues[j]); 96 } 97 } else if (property instanceof byte[][]) { 98 byte[][] bytesValues = (byte[][]) property; 99 for (int j = 0; j < bytesValues.length; j++) { 100 propertyProto.addBytesValues(ByteString.copyFrom(bytesValues[j])); 101 } 102 } else if (property instanceof GenericDocument[]) { 103 GenericDocument[] documentValues = (GenericDocument[]) property; 104 for (int j = 0; j < documentValues.length; j++) { 105 DocumentProto proto = toDocumentProto(documentValues[j]); 106 propertyProto.addDocumentValues(proto); 107 } 108 } else if (property instanceof EmbeddingVector[]) { 109 EmbeddingVector[] embeddingValues = (EmbeddingVector[]) property; 110 for (int j = 0; j < embeddingValues.length; j++) { 111 propertyProto.addVectorValues(embeddingVectorToVectorProto(embeddingValues[j])); 112 } 113 } else if (property instanceof AppSearchBlobHandle[]) { 114 AppSearchBlobHandle[] blobHandleValues = (AppSearchBlobHandle[]) property; 115 for (int j = 0; j < blobHandleValues.length; j++) { 116 propertyProto.addBlobHandleValues( 117 BlobHandleToProtoConverter.toBlobHandleProto(blobHandleValues[j])); 118 } 119 } else if (property == null) { 120 throw new IllegalStateException( 121 String.format("Property \"%s\" doesn't have any value!", name)); 122 } else { 123 throw new IllegalStateException( 124 String.format( 125 "Property \"%s\" has unsupported value type %s", 126 name, property.getClass().toString())); 127 } 128 mProtoBuilder.addProperties(propertyProto); 129 } 130 return mProtoBuilder.build(); 131 } 132 133 /** 134 * Converts a {@link DocumentProto} into a {@link GenericDocument}. 135 * 136 * <p>In the case that the {@link DocumentProto} object proto has no values set, the converter 137 * searches for the matching property name in the {@link SchemaTypeConfigProto} object for the 138 * document, and infers the correct default value to set for the empty property based on the 139 * data type of the property defined by the schema type. 140 * 141 * @param proto the document to convert to a {@link GenericDocument} instance. The document 142 * proto should have its package + database prefix stripped from its fields. 143 * @param prefix the package + database prefix used searching the {@code schemaTypeMap}. 144 * @param schemaCache The SchemaCache instance held in AppSearch. 145 */ 146 @SuppressWarnings("deprecation") toGenericDocument( @onNull DocumentProtoOrBuilder proto, @NonNull String prefix, @NonNull SchemaCache schemaCache, @NonNull AppSearchConfig config)147 public static @NonNull GenericDocument toGenericDocument( 148 @NonNull DocumentProtoOrBuilder proto, 149 @NonNull String prefix, 150 @NonNull SchemaCache schemaCache, 151 @NonNull AppSearchConfig config) 152 throws AppSearchException { 153 Objects.requireNonNull(proto); 154 Objects.requireNonNull(prefix); 155 Objects.requireNonNull(schemaCache); 156 Objects.requireNonNull(config); 157 Map<String, SchemaTypeConfigProto> schemaTypeMap = 158 schemaCache.getSchemaMapForPrefix(prefix); 159 160 GenericDocument.Builder<?> documentBuilder = 161 new GenericDocument.Builder<>( 162 proto.getNamespace(), proto.getUri(), proto.getSchema()) 163 .setScore(proto.getScore()) 164 .setTtlMillis(proto.getTtlMs()) 165 .setCreationTimestampMillis(proto.getCreationTimestampMs()); 166 String prefixedSchemaType = prefix + proto.getSchema(); 167 if (config.shouldRetrieveParentInfo() && !Flags.enableSearchResultParentTypes()) { 168 List<String> parentSchemaTypes = 169 schemaCache.getTransitiveUnprefixedParentSchemaTypes( 170 prefix, prefixedSchemaType); 171 if (!parentSchemaTypes.isEmpty()) { 172 if (config.shouldStoreParentInfoAsSyntheticProperty()) { 173 documentBuilder.setPropertyString( 174 GenericDocument.PARENT_TYPES_SYNTHETIC_PROPERTY, 175 parentSchemaTypes.toArray(new String[0])); 176 } else { 177 documentBuilder.setParentTypes(parentSchemaTypes); 178 } 179 } 180 } 181 182 for (int i = 0; i < proto.getPropertiesCount(); i++) { 183 PropertyProto property = proto.getProperties(i); 184 String name = property.getName(); 185 if (property.getStringValuesCount() > 0) { 186 String[] values = new String[property.getStringValuesCount()]; 187 for (int j = 0; j < values.length; j++) { 188 values[j] = property.getStringValues(j); 189 } 190 documentBuilder.setPropertyString(name, values); 191 } else if (property.getInt64ValuesCount() > 0) { 192 long[] values = new long[property.getInt64ValuesCount()]; 193 for (int j = 0; j < values.length; j++) { 194 values[j] = property.getInt64Values(j); 195 } 196 documentBuilder.setPropertyLong(name, values); 197 } else if (property.getDoubleValuesCount() > 0) { 198 double[] values = new double[property.getDoubleValuesCount()]; 199 for (int j = 0; j < values.length; j++) { 200 values[j] = property.getDoubleValues(j); 201 } 202 documentBuilder.setPropertyDouble(name, values); 203 } else if (property.getBooleanValuesCount() > 0) { 204 boolean[] values = new boolean[property.getBooleanValuesCount()]; 205 for (int j = 0; j < values.length; j++) { 206 values[j] = property.getBooleanValues(j); 207 } 208 documentBuilder.setPropertyBoolean(name, values); 209 } else if (property.getBytesValuesCount() > 0) { 210 byte[][] values = new byte[property.getBytesValuesCount()][]; 211 for (int j = 0; j < values.length; j++) { 212 values[j] = property.getBytesValues(j).toByteArray(); 213 } 214 documentBuilder.setPropertyBytes(name, values); 215 } else if (property.getDocumentValuesCount() > 0) { 216 GenericDocument[] values = new GenericDocument[property.getDocumentValuesCount()]; 217 for (int j = 0; j < values.length; j++) { 218 values[j] = 219 toGenericDocument( 220 property.getDocumentValues(j), prefix, schemaCache, config); 221 } 222 documentBuilder.setPropertyDocument(name, values); 223 } else if (property.getVectorValuesCount() > 0) { 224 EmbeddingVector[] values = new EmbeddingVector[property.getVectorValuesCount()]; 225 for (int j = 0; j < values.length; j++) { 226 values[j] = vectorProtoToEmbeddingVector(property.getVectorValues(j)); 227 } 228 documentBuilder.setPropertyEmbedding(name, values); 229 } else if (property.getBlobHandleValuesCount() > 0) { 230 AppSearchBlobHandle[] values = 231 new AppSearchBlobHandle[property.getBlobHandleValuesCount()]; 232 for (int j = 0; j < values.length; j++) { 233 values[j] = 234 BlobHandleToProtoConverter.toAppSearchBlobHandle( 235 property.getBlobHandleValues(j)); 236 } 237 documentBuilder.setPropertyBlobHandle(name, values); 238 } else { 239 // TODO(b/184966497): Optimize by caching PropertyConfigProto 240 SchemaTypeConfigProto schema = 241 Objects.requireNonNull(schemaTypeMap.get(prefixedSchemaType)); 242 setEmptyProperty(name, documentBuilder, schema); 243 } 244 } 245 return documentBuilder.build(); 246 } 247 248 /** Converts a {@link PropertyProto.VectorProto} into an {@link EmbeddingVector}. */ vectorProtoToEmbeddingVector( PropertyProto.@onNull VectorProto vectorProto)249 public static @NonNull EmbeddingVector vectorProtoToEmbeddingVector( 250 PropertyProto.@NonNull VectorProto vectorProto) { 251 Objects.requireNonNull(vectorProto); 252 253 float[] values = new float[vectorProto.getValuesCount()]; 254 for (int i = 0; i < vectorProto.getValuesCount(); i++) { 255 values[i] = vectorProto.getValues(i); 256 } 257 return new EmbeddingVector(values, vectorProto.getModelSignature()); 258 } 259 260 /** Converts an {@link EmbeddingVector} into a {@link PropertyProto.VectorProto}. */ embeddingVectorToVectorProto( @onNull EmbeddingVector embedding)261 public static PropertyProto.@NonNull VectorProto embeddingVectorToVectorProto( 262 @NonNull EmbeddingVector embedding) { 263 Objects.requireNonNull(embedding); 264 265 PropertyProto.VectorProto.Builder builder = PropertyProto.VectorProto.newBuilder(); 266 for (int i = 0; i < embedding.getValues().length; i++) { 267 builder.addValues(embedding.getValues()[i]); 268 } 269 builder.setModelSignature(embedding.getModelSignature()); 270 return builder.build(); 271 } 272 setEmptyProperty( @onNull String propertyName, GenericDocument.@NonNull Builder<?> documentBuilder, @NonNull SchemaTypeConfigProto schema)273 private static void setEmptyProperty( 274 @NonNull String propertyName, 275 GenericDocument.@NonNull Builder<?> documentBuilder, 276 @NonNull SchemaTypeConfigProto schema) { 277 @AppSearchSchema.PropertyConfig.DataType int dataType = 0; 278 for (int i = 0; i < schema.getPropertiesCount(); ++i) { 279 if (propertyName.equals(schema.getProperties(i).getPropertyName())) { 280 dataType = schema.getProperties(i).getDataType().getNumber(); 281 break; 282 } 283 } 284 285 switch (dataType) { 286 case AppSearchSchema.PropertyConfig.DATA_TYPE_STRING: 287 documentBuilder.setPropertyString(propertyName, EMPTY_STRING_ARRAY); 288 break; 289 case AppSearchSchema.PropertyConfig.DATA_TYPE_LONG: 290 documentBuilder.setPropertyLong(propertyName, EMPTY_LONG_ARRAY); 291 break; 292 case AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE: 293 documentBuilder.setPropertyDouble(propertyName, EMPTY_DOUBLE_ARRAY); 294 break; 295 case AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN: 296 documentBuilder.setPropertyBoolean(propertyName, EMPTY_BOOLEAN_ARRAY); 297 break; 298 case AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES: 299 documentBuilder.setPropertyBytes(propertyName, EMPTY_BYTES_ARRAY); 300 break; 301 case AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT: 302 documentBuilder.setPropertyDocument(propertyName, EMPTY_DOCUMENT_ARRAY); 303 break; 304 case AppSearchSchema.PropertyConfig.DATA_TYPE_EMBEDDING: 305 documentBuilder.setPropertyEmbedding(propertyName, EMPTY_EMBEDDING_ARRAY); 306 break; 307 default: 308 throw new IllegalStateException("Unknown type of value: " + propertyName); 309 } 310 } 311 } 312