Skip to main content

Photo Search

The Photo SDK provides a search engine that is described in this article.

Search For Photos And Albums By Suggestions#

To search for photos and/or albums, the steps are the following:

  1. Get text input from the user;
  2. Supply entered text to the Photos SDK;
  3. In return, Photos SDK provides list of Suggestions;
  4. Pass one or all of the suggestions to the Photo SDK;
  5. Receive list of photos and/or albums which are relevant to given suggestion(s).

Here is the explanation of these steps in the code.

Chaining User Input To Suggestions#

The following snippet shows how to create fragment and convert user input to suggestions.

class SuggestionsFragment : Fragment() {
    private val disposable = CompositeDisposable()    private val search: Search = PhotoManager.search
    private val adapter = SuggestionsAdapter()  // see source code at the end of the chapter    private var currentSuggestions: SuggestionContainer? = null
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)
        // Initialize recycler view        view.recyclerView.layoutManager = LinearLayoutManager(context)        view.recyclerView.adapter = adapter
        // Prepare observable that emits new string every time user changes the text        val queryObservable =  RxTextView.textChanges(view.queryView)            .map { it.toString() }            .startWith("")
        // Chain text observable to suggestions observable         disposable += search.chainSuggestionsObservable(            queryObservable,            SuggestionQueryConfig("en")        )            .observeOn(AndroidSchedulers.mainThread())            .subscribe { suggestionContainer ->                // We receive new suggestion container every time user changes their input                currentSuggestions = suggestionContainer                adapter.data = suggestionContainer.toList()            }
        // Handle tap on SuggestionItem in recycler view        adapter.clickListener = object : SearchSugAdapter.ClickListener {            override fun onItemClicked(sugItem: SuggestionItem, position: Int) {                onSuggestionClicked(sugItem) // defined later            }        }
        // Handle 'enter' or 'search' key of virtual keyboard        disposable += RxTextView.editorActions(view.queryView) { actionId ->            actionId == EditorInfo.IME_ACTION_SEARCH        }.subscribe {            // User pressed enter (or 'Search' button)            currentSuggestions?.let { onSearchClicked(it) } // defined later        }    }
    override fun onDestroyView() {        // Dispose all subscriptions        disposable.clear()        super.onDestroyView()    }}

By chaining user input to suggestions using search.chainSuggestionsObservable(observable, config), app makes Photos SDK to send query to backend every time when user's input is changed. In response backend sends list of suggestions which match given string, then Photos SDK converts them to SuggestionContainer and returns it to the app.

Querying By Suggestions#

The following code snippet shows how to make search query using suggestions:

private fun onSuggestionClicked(suggestionItem: SuggestionItem) {    disposable += search.searchBySuggestion(suggestionItem)        .subscribeBy(            onComplete = {                // Search result is read from backend and ready for display            },            onError = {                // Handle it            }        )}
private fun onSearchClicked(container: SuggestionContainer) {    disposable += search.searchBySuggestions(container)        .subscribeBy(            onComplete = {                // Search results are read from backend and ready for display            },            onError = {                // Handle it            }        )}

Note these functions return Completables which do not emit any photos nor albums. Completion indicates that fetching of search results is done, nothing more.

Next chapter explains how to get and display those results.

Displaying The Search Result#

Suggestion item can be one of the following types:

  • AddressSuggestionItem - based on geo-location of media
  • CalendarSuggestionItem - based on date/time when photo or video was taken
  • LabelSuggestionItem - based on media item description (this is not album description!)
  • AlbumSuggestionItem - based on album description

Note that only AlbumSuggestionItem is related to albums, while other 3 types are related solely to media items (photos and videos).

When your app makes a search by calling of searchBySuggestion(suggestionItem) or searchBySuggestions(container), depending on which type of suggestion(s) was given, Photos SDK provides list of either media items (photos/videos) or albums or both of them (when SuggestionContainer holds suggestions of both media- and album-related types).

Getting the search results is quite similar to getting list of media items from Timeline and getting album list from Albums components of the Photos SDK.

The following snippet shows how to get search result of media items:

private class SearchPhotosViewModel(    private val search: Search) : ViewModel() {
    val contentObservable: Observable<PagingData<PhotoItem>> by lazy {        search.createPagingPhotosObservable()            .cachedIn(viewModelScope)    }}
private class SearchPhotosViewModelFactory(    private val search: Search) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {        return SearchPhotosViewModel(search) as T    }}
class SearchResultPhotoFrg : Fragment() {
    private var disposable: Disposable? = null    private val adapter: TimelineAdapter // derived from PagingDataAdapter    private val search: Search = PhotoManager.search    private val viewModel: SearchPhotosViewModel by viewModels { SearchPhotosViewModelFactory(search) }

    override fun onStart() {        super.onStart()        // You have to subscribe to the returned Observable to receive updates.        disposable = viewModel.contentObservable            .subscribe { pagingData ->                // Update adapter                adapter.submitData(this.lifecycle, pagingData)            }    }
    override fun onStop() {        super.onStop()        // You must call dispose() on disposable that returned by the subscribe() method,        // when it is no longer needed, for example in your fragment’s onStop() or onPause().        disposable?.dispose()    }}

Note that Observable returned by the method search.createPagingPhotosObservable() emits non-empty PagingData when fetching of search results from backend is completed. Before that moment Observable might emit empty content.

Search component of Photos SDK provides a number of methods for getting search results.

The following methods can be used to get albums after querying by album-related suggestion(s):

  • createPagingAlbumsObservable(): Observable<PagingData<AlbumItem>> - returns Observable
  • createPagingAlbumsFlow(): Flow<PagingData<AlbumItem>> - returns Flow

The following methods can be used to get media items (photos and videos) after querying by media items-related suggestions(s):

  • createPagingPhotosObservable(): Observable<PagingData<PhotoItem>> - returns Observable
  • createPagingPhotosFlow(): Flow<PagingData<PhotoItem>> - returns Flow

Observables and Flows returned by those methods should be cached in view model as shown in example above.

Adapter For Suggestion List Fragment#

To demonstrate, here is SuggestionsAdapter:

class SuggestionsAdapter : RecyclerView.Adapter<SearchSugAdapter.SuggestionVH>() {
    var data: List<SuggestionItem> = emptyList()        set(d) {            field = d            notifyDataSetChanged()        }
    interface ClickListener {        fun onItemClicked(sugItem: SuggestionItem, position: Int)    }
    // Must be initialized in fragment    lateinit var clickListener: ClickListener
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =        SuggestionVH(LayoutInflater.from(parent.context).inflate(R.layout.item_suggestion_item, parent, false))
    override fun onBindViewHolder(holder: SuggestionVH, position: Int) {        holder.bind(data[position], position, clickListener)    }
    override fun getItemCount() = data.size
    override fun getItemId(position: Int): Long = data[position].id.toLong()
    class SuggestionVH(private val view: View) : RecyclerView.ViewHolder(view) {
        fun bind(sug: SuggestionItem, position: Int, clickListener: ClickListener) {            // Item view has only 3 fields: name, date and type            view.nameView.text = sug.name            view.dateView.text = "updated: " + SimpleDateFormat.getDateTimeInstance().format(Date(sug.updatedAt))            view.typeView.text = when (sug) {                is AddressSuggestionItem -> "address"                is AlbumSuggestionItem -> "album"                is CalendarSuggestionItem -> "calendar"                is LabelSuggestionItem -> "label"                else -> throw IllegalArgumentException("Unknown suggestion type $sug")            }            view.setOnClickListener {                clickListener.onItemClicked(sug, position)            }        }    }}

Search By Faces#

To search for a photo by faces, the first step is to get a list of faces from the photo. Then, get a list of photos for the selected person.

Get Faces From Photo#

To extract faces from photo item, use the following code example.

var disposable: Disposable? = null
// PhotoItems to download to your local device.var photoItem: PhotoItem? = null
// ...
// Returns Single, which completes when the operation ends.// You have to subscribe to returned Single in order to receive updates.disposable = PhotoManager.timeline.getFacesFromPhoto(photoItem)        .observeOn(AndroidSchedulers.mainThread())        .subscribeBy(            onSuccess = { // it: List<FaceItem>!                // Handle it            },            onError = { // it: Throwable                // Handle it            }        )
// ...
// You must call dispose() on disposable that returned by subscribe() method,// when it is no longer needed, for example in your fragment’s onStop() or onPause().disposable?.dispose()

Links: PhotoItem, FaceItem.

Find Photos By Face#

To find a photo by face, use the following code example.

var disposable: Disposable? = null
// FaceItem to getting photo list.var faceItem: FaceItem? = null
// ...
// Returns Single, which completes when the operation ends.// You have to subscribe to returned Single in order to receive updates.disposable = timeline.search.findPhotosByFace(faceItem)        .observeOn(AndroidSchedulers.mainThread())        .subscribeBy(            onSuccess = { // it: List<FaceItem>!                // Handle it            },            onError = { // it: Throwable                // Handle it            }        )
// ...
// You must call dispose() on disposable that returned by subscribe() method,// when it is no longer needed, for example in your fragment’s onStop() or onPause().disposable?.dispose()

Links: FaceItem.