Pull to refresh

Полнотекстовый поиск на App Engine уже сейчас

Google App Engine *
Все конечно с нетерпением ждут, когда в App Engine появится полнотекстовый поиск, но пока его нет даже в roadmap. Тем не менее, для GAE/Java полнотекстовый поиск можно прикрутить самостоятельно уже сейчас.



Вы удивитесь, но это Lucene. Нет-нет, не спешите разочарованно уходить, вздыхая «ну это ж сколько возиться надо». Подружить Lucene с App Engine datastore дело, наверное, не пяти минут, но если вы используете JDO, то за вас это уже сделали разработчики compass-project в разрабатываемом compass 2.3 (сейчас в стадии бета-версии).

Compass умеет превращать ваши JDO объекты в проиндексированные документы и возвращать их в качестве результатов поиска. Код простейшего JDO объекта с индексированием и поиском, и комментарии к интересным местам в нём смотрите ниже. Чтобы это скомпилировалось и заработало, нужно добавить в war/WEB-INF/lib несколько библиотек: lucene-core, commons-logging и compass-2.3.0-beta1. Они все живут в дистрибутиве ночного билда Compass'а, который закопан глубоко в дебри их continuous build системы. На момент написания, последний успешный билд живёт тут: http://build.compass-project.org/download/CMPTRK-NIGHTLY/artifacts/build-786/Release. Признаюсь что я не проверял работоспособность конкретно этого билда.

@PersistenceCapable(identityType = IdentityType.APPLICATION)
@Searchable // [1]
public class GreetingServiceUser {
  @PrimaryKey
  @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
  @SearchableId
  private Long id; // [2]

  @Persistent
  private String name;

  public GreetingServiceUser(String name) {
    this.name = name;
  }

  @SearchableProperty // [3]
  public String getName() {
    return name;
  }

  private static final PersistenceManagerFactory factory =
    JDOHelper.getPersistenceManagerFactory("transactions-optional");
  private static Compass compass;
  private static CompassGps compassGps;

  static {
    // [4]
    compass = new CompassConfiguration()
      .setConnection("gae://index")
      // [5]
      .setSetting(CompassEnvironment.ExecutorManager.EXECUTOR_MANAGER_TYPE,
          "disabled")
      .addScan("compass_test")
      .buildCompass();
    compassGps = new SingleCompassGps(compass);
    Jdo2GpsDevice jdo2GpsDevice = new Jdo2GpsDevice("appengine", factory);
    // [6]
    jdo2GpsDevice.setMirrorDataChanges(false);
    compassGps.addGpsDevice(jdo2GpsDevice);
    // [7]
    // if (compass.getSearchEngineIndexManager().isLocked()) {
    //   compass.getSearchEngineIndexManager().releaseLocks();
    // }
    compassGps.start();
  }

  public void save() {
    PersistenceManager pm = factory.getPersistenceManager();
    CompassIndexSession indexSession = null;
    try {
      pm.makePersistent(this);

      // [8]
      // compass.getSearchEngineIndexManager().releaseLocks();

      // [9]
      indexSession = compass.openIndexSession();
      indexSession.save(this);
      indexSession.commit();
    }
    catch (Throwable e) {
      e.printStackTrace();
    }
    finally {
      pm.close();
      if (indexSession != null) {
        indexSession.close();
      }
    }
  }

  public static List<GreetingServiceUser> search(String query) {
    CompassSearchSession searchSession = compass.openSearchSession();

    CompassHits hits = searchSession.find(query);
    List<GreetingServiceUser> results =
      new ArrayList<GreetingServiceUser>(hits.getLength());
    PersistenceManager pm = factory.getPersistenceManager();
    try {
      for (int i=0; i<hits.length(); i++) {
        // [10]
        results.add(pm.getObjectById(
            GreetingServiceUser.class,
            Long.valueOf(hits.resource(i).getId())));
      }
    }
    catch (JDOObjectNotFoundException e) {
      e.printStackTrace();
    }
    finally {
      pm.close();
    }
    return results;
  }
}

* This source code was highlighted with Source Code Highlighter.


[1] Аннотациями отмечаются JDO классы, подлежащие индексированию…
[2]… свойства, идентифицирующие документы,…
[3]… и наконец свойства, значения которых будут проиндексированы.
[4] При конструировании экземпляра Compass вы указываете, какие пакеты надо просканировать в поисках таких аннотаций
[5] Это тонкость, специфичная для App Engine. Compass вообще работает в несколько потоков, но на App Engine потоки создавать нельзя, поэтому нужно потоки запретить.
[6] Это тоже небольшой хак актуальный для App Engine. Compass вообще-то умеет актуализировать индекс автоматически, когда что-нибудь меняется в интересных ему персистентных данных. Но при работе на App Engine он может не уложиться в отведённые 30 секунд. Вызов setMirrorDataChanges(false) отключит автоматическое обновление и обновлять индекс нужно будет вручную. Это не больно, и в качестве компенсации это можно делать, например, в отдельной задаче в task queue.
[7, 8] Поддержка App Engine ещё сыровата, и иногда Compass, кажется, забывает снимать блокировки индекса. В причинах и правильном способе исправления я пока не разбирался, но явная просьба отпустить блокировки помогает. Надо, конечно, понимать что это может быть не очень безопасно. Если кто найдёт время проблему отловить и победить, дайте знать.
[9] Собственно так объект индексируется вручную.
[10] Результатом поиска является коллекция CompassHits. Из неё теоретически можно извлечь найденные объекты (GreetingServiceUser в нашем случае) методом data(int), но у меня они получались с незаполненными значениями. Поэтому я пользуюсь объектом Resource, который соответствует документу, извлекаю из него id документа и ищу соответствующий JDO объект средствами JDO/AppEngine.
Tags:
Hubs:
Total votes 9: ↑6 and ↓3 +3
Views 831
Comments Comments 9