prepareSearchResultsJapaneseLanguage function

Future<int?> prepareSearchResultsJapaneseLanguage(
  1. DictionarySearchParams params
)

Top-level function for use in compute. See Language for details. Credits to Matthew Chan for their port of the Yomichan parser to Dart. Top-level function for use in compute. See Language for details. Credits to Matthew Chan for their port of the Yomichan parser to Dart.

Implementation

Future<int?> prepareSearchResultsJapaneseLanguage(
    DictionarySearchParams params) async {
  int bestLength = 0;
  String searchTerm = params.searchTerm.trim();
  const KanaKit kanaKit = KanaKit();
  if (kanaKit.isRomaji(searchTerm)) {
    searchTerm = kanaKit.toHiragana(searchTerm);
  }

  if (searchTerm.length > 20) {
    searchTerm = searchTerm.substring(0, 20);
  }

  int maximumHeadings = params.maximumDictionarySearchResults;

  if (searchTerm.isEmpty) {
    return null;
  }

  final Isar database = await Isar.open(
    globalSchemas,
    directory: params.directoryPath,
    maxSizeMiB: 8192,
  );

  Map<int, DictionaryHeading> uniqueHeadingsById = {};

  int limit() {
    return maximumHeadings - uniqueHeadingsById.length;
  }

  bool shouldSearchWildcards = params.searchWithWildcards &&
      (searchTerm.contains('※') ||
          searchTerm.contains('?') ||
          searchTerm.contains('*') ||
          searchTerm.contains('?'));

  if (shouldSearchWildcards) {
    bool noExactMatches = database.dictionaryHeadings
        .where()
        .termEqualTo(searchTerm)
        .isEmptySync();

    if (noExactMatches) {
      String matchesTerm = searchTerm
          .replaceAll('※', '*')
          .replaceAll('?', '?')
          .replaceAll('?', '???');

      List<DictionaryHeading> termMatchHeadings = [];
      List<DictionaryHeading> readingMatchHeadings = [];
      String withoutWildcards =
          matchesTerm.replaceAll('?', '').replaceAll('*', '');
      bool matchTermIsKana = kanaKit.isKana(withoutWildcards);
      bool matchTermIsKatakana = kanaKit.isKatakana(withoutWildcards);

      bool questionMarkOnly = !matchesTerm.contains('*');
      String noAsterisks = searchTerm
          .replaceAll('※', '*')
          .replaceAll('?', '?')
          .replaceAll('*', '');

      if (params.maximumDictionaryTermsInResult > uniqueHeadingsById.length) {
        if (questionMarkOnly) {
          termMatchHeadings = database.dictionaryHeadings
              .where()
              .termLengthEqualTo(searchTerm.length)
              .filter()
              .termMatches(matchesTerm, caseSensitive: false)
              .and()
              .entriesIsNotEmpty()
              .limit(maximumHeadings - uniqueHeadingsById.length)
              .findAllSync();
        } else {
          termMatchHeadings = database.dictionaryHeadings
              .where()
              .termLengthGreaterThan(noAsterisks.length, include: true)
              .filter()
              .termMatches(matchesTerm, caseSensitive: false)
              .and()
              .entriesIsNotEmpty()
              .limit(maximumHeadings - uniqueHeadingsById.length)
              .findAllSync();
        }
      }

      uniqueHeadingsById.addEntries(
        termMatchHeadings.map(
          (heading) => MapEntry(heading.id, heading),
        ),
      );

      if (params.maximumDictionaryTermsInResult > uniqueHeadingsById.length) {
        if (matchTermIsKana) {
          readingMatchHeadings = database.dictionaryHeadings
              .where()
              .filter()
              .entriesIsNotEmpty()
              .readingMatches(matchesTerm)
              .or()
              .optional(matchTermIsKatakana,
                  (q) => q.readingMatches(kanaKit.toHiragana(matchesTerm)))
              .and()
              .entriesIsNotEmpty()
              .findAllSync();

          uniqueHeadingsById.addEntries(
            readingMatchHeadings.map(
              (heading) => MapEntry(heading.id, heading),
            ),
          );
        }
      }
    }
  } else {
    List<String> deinflectionsAlreadySearched = [];

    bool startsWithAdded = false;
    for (int i = 0; i < searchTerm.length; i++) {
      String partialTerm = searchTerm.substring(0, searchTerm.length - i);

      bool partialTermIsKana = kanaKit.isKana(partialTerm);
      bool partialTermIsHiragana = kanaKit.isHiragana(partialTerm);
      bool partialTermIsKatakana = kanaKit.isKatakana(partialTerm);

      List<String> possibleDeinflections = Deinflector.deinflect(partialTerm)
          .map((e) => e.term)
          .where((e) => !deinflectionsAlreadySearched.contains(e))
          .toList();
      possibleDeinflections.toSet().addAll(deinflectionsAlreadySearched);

      List<DictionaryHeading> termExactResults = [];
      List<DictionaryHeading> termDeinflectedResults = [];
      List<DictionaryHeading> readingExactResults = [];
      List<DictionaryHeading> readingDeinflectedResults = [];
      List<DictionaryHeading> termExactKatakanaResults = [];

      if (params.maximumDictionaryTermsInResult > uniqueHeadingsById.length) {
        /// If an exception is caught, skip the iteration entirely. Something
        /// has gone wrong with building a query for this term and there is
        /// no point continuing for the rest of the queries.
        try {
          termExactResults = database.dictionaryHeadings
              .where()
              .termEqualTo(partialTerm)
              .or()
              .optional(partialTermIsKatakana,
                  (q) => q.termEqualTo(kanaKit.toHiragana(partialTerm)))
              .filter()
              .entriesIsNotEmpty()
              .findAllSync();
        } catch (e) {
          await Future.delayed(const Duration(milliseconds: 200), () {});
          continue;
        }

        uniqueHeadingsById.addEntries(
          termExactResults.map(
            (heading) => MapEntry(heading.id, heading),
          ),
        );
      }

      if (params.maximumDictionaryTermsInResult > uniqueHeadingsById.length) {
        if (partialTermIsKana) {
          readingExactResults = database.dictionaryHeadings
              .where()
              .readingEqualTo(partialTerm)
              .or()
              .optional(partialTermIsKatakana,
                  (q) => q.readingEqualTo(kanaKit.toHiragana(partialTerm)))
              .filter()
              .entriesIsNotEmpty()
              .findAllSync();

          uniqueHeadingsById.addEntries(
            readingExactResults.map(
              (heading) => MapEntry(heading.id, heading),
            ),
          );
        }
      }

      if (params.maximumDictionaryTermsInResult > uniqueHeadingsById.length) {
        termDeinflectedResults = database.dictionaryHeadings
            .where()
            .anyOf<String, String>(
                possibleDeinflections, (q, term) => q.termEqualTo(term))
            .or()
            .optional(
                partialTermIsKatakana,
                (q) => q.anyOf<String, String>(
                    Deinflector.deinflect(kanaKit.toHiragana(partialTerm))
                        .map((e) => e.term)
                        .toList(),
                    (q, term) => q.termEqualTo(term)))
            .filter()
            .entriesIsNotEmpty()
            .findAllSync();

        for (DictionaryHeading result in termDeinflectedResults) {
          result.entries.loadSync();
        }
        termDeinflectedResults
            .sort((a, b) => b.popularitySum.compareTo(a.popularitySum));

        uniqueHeadingsById.addEntries(
          termDeinflectedResults.map(
            (heading) => MapEntry(heading.id, heading),
          ),
        );
      }

      if (params.maximumDictionaryTermsInResult > uniqueHeadingsById.length) {
        if (partialTermIsKana) {
          readingDeinflectedResults = database.dictionaryHeadings
              .where()
              .anyOf<String, String>(possibleDeinflections,
                  (q, reading) => q.readingEqualTo(reading))
              .or()
              .optional(
                  partialTermIsKatakana,
                  (q) => q.anyOf<String, String>(
                      Deinflector.deinflect(kanaKit.toHiragana(partialTerm))
                          .map((e) => e.term)
                          .toList(),
                      (q, term) => q.readingEqualTo(term)))
              .filter()
              .entriesIsNotEmpty()
              .limit(limit())
              .findAllSync();

          uniqueHeadingsById.addEntries(
            readingDeinflectedResults.map(
              (heading) => MapEntry(heading.id, heading),
            ),
          );
        }
      }

      if (params.maximumDictionaryTermsInResult > uniqueHeadingsById.length) {
        if (partialTermIsHiragana) {
          termExactKatakanaResults = database.dictionaryHeadings
              .where()
              .termEqualTo(kanaKit.toKatakana(partialTerm))
              .filter()
              .entriesIsNotEmpty()
              .findAllSync();

          uniqueHeadingsById.addEntries(
            termExactKatakanaResults.map(
              (heading) => MapEntry(heading.id, heading),
            ),
          );
        }
      }

      if (termExactResults.isNotEmpty && bestLength < partialTerm.length) {
        bestLength = partialTerm.length;
      }
      if (termDeinflectedResults.isNotEmpty &&
          bestLength < partialTerm.length) {
        bestLength = partialTerm.length;
      }
      if (readingExactResults.isNotEmpty && bestLength < partialTerm.length) {
        bestLength = partialTerm.length;
      }
      if (readingDeinflectedResults.isNotEmpty &&
          bestLength < partialTerm.length) {
        bestLength = partialTerm.length;
      }
      if (termExactKatakanaResults.isNotEmpty &&
          bestLength < partialTerm.length) {
        bestLength = partialTerm.length;
      }

      if (params.maximumDictionaryTermsInResult > uniqueHeadingsById.length) {
        if (params.searchWithWildcards) {
          if (i == 0) {
            startsWithAdded = true;

            List<DictionaryHeading> startsWithToAdd = database
                .dictionaryHeadings
                .where()
                .termStartsWith(searchTerm)
                .filter()
                .entriesIsNotEmpty()
                .sortByTermLength()
                .findAllSync();

            uniqueHeadingsById.addEntries(startsWithToAdd.map(
              (heading) => MapEntry(heading.id, heading),
            ));
          }
        } else {
          if (!startsWithAdded && uniqueHeadingsById.isNotEmpty) {
            startsWithAdded = true;

            List<DictionaryHeading> startsWithToAdd = database
                .dictionaryHeadings
                .where()
                .termStartsWith(searchTerm)
                .filter()
                .entriesIsNotEmpty()
                .sortByTermLength()
                .findAllSync();

            uniqueHeadingsById.addEntries(startsWithToAdd.map(
              (heading) => MapEntry(heading.id, heading),
            ));
          }
        }
      }
    }
  }

  List<DictionaryHeading> headings =
      uniqueHeadingsById.values.where((e) => e.entries.isNotEmpty).toList();
  Map<DictionaryHeading, int> headingOrders = Map.fromEntries(
    headings.mapIndexed(
      (index, id) => MapEntry(headings[index], index),
    ),
  );

  if (headings.isEmpty) {
    return null;
  }

  DictionarySearchResult unsortedResult = DictionarySearchResult(
    searchTerm: searchTerm,
    bestLength: bestLength,
  );
  unsortedResult.headings.addAll(headings);

  late int resultId;
  database.writeTxnSync(() async {
    database.dictionarySearchResults.deleteBySearchTermSync(searchTerm);
    resultId = database.dictionarySearchResults.putSync(unsortedResult);
  });

  preloadResultSync(resultId);

  List<Dictionary> dictionaries = database.dictionarys.where().findAllSync();

  Map<String, int> dictionaryNamesByOrder = Map<String, int>.fromEntries(
      dictionaries.map((e) => MapEntry(e.name, e.order)));

  headings.sort((a, b) {
    if (a.term == b.term ||
        (a.reading.isNotEmpty && b.reading.isNotEmpty) &&
            a.reading == b.reading) {
      int hasPopularTag = (a.tags.any((e) => e.name == 'P') ? -1 : 1)
          .compareTo(b.tags.any((e) => e.name == 'P') ? -1 : 1);
      if (hasPopularTag != 0) {
        return hasPopularTag;
      }

      if (a.term != b.term) {
        int popularityCompare = (b.popularitySum).compareTo(a.popularitySum);
        if (popularityCompare != 0) {
          return popularityCompare;
        }
      }

      List<DictionaryFrequency> aFrequencies = a.frequencies.toList();
      List<DictionaryFrequency> bFrequencies = b.frequencies.toList();
      aFrequencies.sort((a, b) =>
          dictionaryNamesByOrder[a.dictionary.value!.name]!
              .compareTo(dictionaryNamesByOrder[b.dictionary.value!.name]!));
      bFrequencies.sort((a, b) =>
          dictionaryNamesByOrder[a.dictionary.value!.name]!
              .compareTo(dictionaryNamesByOrder[b.dictionary.value!.name]!));

      Map<Dictionary, List<DictionaryFrequency>> aFrequenciesByDictionary =
          groupBy<DictionaryFrequency, Dictionary>(
              aFrequencies, (frequency) => frequency.dictionary.value!);
      Map<Dictionary, List<DictionaryFrequency>> bFrequenciesByDictionary =
          groupBy<DictionaryFrequency, Dictionary>(
              bFrequencies, (frequency) => frequency.dictionary.value!);
      Map<Dictionary, double> aValues = aFrequenciesByDictionary
          .map((k, v) => MapEntry(k, v.map((e) => e.value).min));
      Map<Dictionary, double> bValues = bFrequenciesByDictionary
          .map((k, v) => MapEntry(k, v.map((e) => e.value).min));

      Set<Dictionary> sharedDictionaries =
          aValues.keys.toSet().intersection(bValues.keys.toSet());

      if (sharedDictionaries.isNotEmpty) {
        for (Dictionary dictionary in sharedDictionaries) {
          int freqCompare =
              aValues[dictionary]!.compareTo(bValues[dictionary]!);
          if (freqCompare != 0) {
            return freqCompare;
          }
        }
      } else {
        int popularityCompare = (b.popularitySum).compareTo(a.popularitySum);
        if (popularityCompare != 0) {
          return popularityCompare;
        }
      }

      int entriesCompare = b.entries.length.compareTo(a.entries.length);
      if (entriesCompare != 0) {
        return entriesCompare;
      }
    }

    return headingOrders[a]!.compareTo(headingOrders[b]!);
  });

  /// Prioritise kanji match.
  if (searchTerm.length == 1 && kanaKit.isKanji(searchTerm)) {
    DictionaryHeading? heading = database.dictionaryHeadings
        .getSync(DictionaryHeading.hash(term: searchTerm, reading: ''));
    if (heading != null && heading.entries.isNotEmpty) {
      headings.remove(heading);
      headings.insert(0, heading);
    }
  }

  headings = headings.sublist(
      0, min(headings.length, params.maximumDictionaryTermsInResult));

  List<int> headingIds = headings.map((e) => e.id).toList();

  DictionarySearchResult result = DictionarySearchResult(
    id: resultId,
    searchTerm: searchTerm,
    bestLength: bestLength,
    headingIds: headingIds,
  );

  database.writeTxnSync(() async {
    resultId = database.dictionarySearchResults.putSync(result);

    int countInSameHistory = database.dictionarySearchResults.countSync();

    if (params.maximumDictionarySearchResults < countInSameHistory) {
      int surplus = countInSameHistory - params.maximumDictionarySearchResults;
      database.dictionarySearchResults
          .where()
          .limit(surplus)
          .build()
          .deleteAllSync();
    }
  });

  return resultId;
}