これまでに、Apache Solrとcrawler4jでHTMLをクローリングしてインデックスするのと、Apache Tikaを使ってPDFを読んでみるエントリを書いてみました。
Apache Solr 5.x+crawler4jで、Webサイトをクロールしてインデックス化する - CLOVER
今度は、これらを使ってHTMLとPDFをクローリングしてSolrのインデックスを作ってみたいと思います。
やり方
全体の流れは、以下のようにします。
- VMwareのドキュメントサイトの一部(http://info.vmware.com/content/apac_jp_co_techresources)をクローリング
- HTMLとPDFを対象にする
- インデックス作成は、いきなりSolrjでドキュメントを追加するのではなく、いったんJSONをファイルで作成
- クローリング終了後に、作成したJSONファイルから一気にUpdateHandlerを使ってSolrのインデックス更新
こんな感じで。
準備
Maven依存関係は、このように。
<dependencies> <dependency> <groupId>edu.uci.ics</groupId> <artifactId>crawler4j</artifactId> <version>4.1</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.12</version> </dependency> <dependency> <groupId>org.apache.solr</groupId> <artifactId>solr-solrj</artifactId> <version>5.3.1</version> </dependency> <dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.8.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.6.3</version> </dependency> </dependencies>
HTML解析にjsoupを使い、JSON作成にJackson2を使用。Apache Tikaは、crawler4jの依存関係に含まれているので、明示的には入れていません。
Solrのインデックス定義
Solrのインデックス定義は、このようにしました。
<field name="url" type="string" indexed="true" stored="true" required="true" multiValued="false" /> <field name="title" type="text_ja" indexed="true" stored="true" required="true" multiValued="false" /> <field name="description" type="text_ja" indexed="true" stored="true" required="false" multiValued="false" /> <field name="contents" type="text_ja" indexed="true" stored="true" required="true" multiValued="false" /> <field name="file_type" type="string" indexed="true" stored="true" required="true" multiValued="false" /> <uniqueKey>url</uniqueKey>
URLとドキュメントのタイトル、description(HTMLしかありませんが…)、コンテンツの中身、そしてファイルの種類(html or pdf)とします。
この中に、解析したHTMLとPDFを放り込みます。
クローラーの実装
それでは、各種Javaコードを書いていきます。
クロール時に作成する、ドキュメントとなるクラスをこのように定義。
src/main/java/org/littlewings/mycrawler/CrawlDocument.java
package org.littlewings.mycrawler; public class CrawlDocument { private String url; private String title; private String description; private String contents; private String fileType; CrawlDocument(String url, String title, String description, String contents, String fileType) { this.url = url; this.title = title; this.description = description; this.contents = contents; this.fileType = fileType; } public static CrawlDocument create(String url, String title, String description, String contents, String fileType) { return new CrawlDocument(url, title, description, contents, fileType); } public String getUrl() { return url; } public String getTitle() { return title; } public String getDescription() { return description; } public String getContents() { return contents; } public String getFileType() { return fileType; } }
まあ、Solrのインデックス定義の写しですね。
JSONを作成するためのクラスは、こんな感じで。クローリングしつつ、JSONファイルを作成するという関係上、ObjectMapperではなくJsonGeneratorでStreaming的に書き出します。
※だいぶ乱暴な作りですが…
src/main/java/org/littlewings/mycrawler/JsonWriter.java
package org.littlewings.mycrawler; import java.io.IOException; import java.io.Writer; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategy; public class JsonWriter implements AutoCloseable { private static JsonWriter instance; private ObjectMapper objectMapper; private JsonGenerator jsonGenerator; JsonWriter(Writer writer) throws IOException { objectMapper = new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false) .setSerializationInclusion(JsonInclude.Include.NON_NULL) .setSerializationInclusion(JsonInclude.Include.NON_EMPTY) .setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES); jsonGenerator = objectMapper .getFactory() .createGenerator(writer) .setPrettyPrinter(new DefaultPrettyPrinter()); } public static synchronized JsonWriter getInstance() { return instance; } public static synchronized JsonWriter getInstance(Writer writer) throws IOException { if (instance == null) { instance = new JsonWriter(writer); } return instance; } public synchronized void init() throws IOException { jsonGenerator.writeStartArray(); } public synchronized void write(Object target) throws IOException { jsonGenerator.writeObject(target); } public synchronized void end() throws IOException { jsonGenerator.writeEndArray(); } @Override public synchronized void close() throws Exception { jsonGenerator.close(); } }
クローラーの実装。
src/main/java/org/littlewings/mycrawler/MyLittleCrawler.java
package org.littlewings.mycrawler; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.regex.Pattern; import edu.uci.ics.crawler4j.crawler.Page; import edu.uci.ics.crawler4j.crawler.WebCrawler; import edu.uci.ics.crawler4j.parser.HtmlParseData; import edu.uci.ics.crawler4j.url.WebURL; import org.apache.tika.exception.TikaException; import org.apache.tika.metadata.Metadata; import org.apache.tika.metadata.TikaCoreProperties; import org.apache.tika.parser.ParseContext; import org.apache.tika.parser.Parser; import org.apache.tika.parser.pdf.PDFParser; import org.apache.tika.sax.BodyContentHandler; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; public class MyLittleCrawler extends WebCrawler { private static final Pattern FILTERS = Pattern.compile(".*(\\.(css|js|gif|jpg" + "|png|mp3|mp3|zip|gz))$"); private static final Pattern[] INTERST_PATTERNS = { Pattern.compile("^http://info\\.vmware\\.com/content/apac_jp_co_techresources"), Pattern.compile("^http://www\\.vmware\\.com/files/jp/pdf"), Pattern.compile("^http://info\\.vmware\\.com/content/") }; private Logger logger = LoggerFactory.getLogger(getClass()); @Override public boolean shouldVisit(Page referringPage, WebURL url) { String hrefUrl = url.getURL(); return !FILTERS.matcher(hrefUrl).matches() && Arrays.stream(INTERST_PATTERNS).anyMatch(pattern -> pattern.matcher(hrefUrl).find()); } @Override public void visit(Page page) { String url = page.getWebURL().getURL(); logger.info("URL: {}, Content-Type: {}", url, page.getContentType()); if (page.getParseData() instanceof HtmlParseData) { logger.info("Contents is HTML: URL = {}", url); HtmlParseData htmlParseData = (HtmlParseData) page.getParseData(); String html = htmlParseData.getHtml(); Set<WebURL> links = htmlParseData.getOutgoingUrls(); logger.info("Number of outgoing links: {}", links.size()); Document jsoupDoc = Jsoup.parse(html); String title = jsoupDoc.title(); String description = jsoupDoc.select("meta[name=\"description\"]").attr("content"); String contents = jsoupDoc.body().text(); if (title != null && !title.isEmpty() && contents != null && !contents.isEmpty()) { try { JsonWriter.getInstance().write(CrawlDocument.create(url, title, description, contents, "html")); } catch (IOException e) { throw new UncheckedIOException(e); } } else { logger.info("Invalid HTML title: {}, contents: {}, URL = {}", title, contents, url); } } else if (page.getContentType().startsWith("application/pdf") || url.endsWith(".pdf")) { logger.info("Contents is PDF: URL = {}", url); InputStream is = new ByteArrayInputStream(page.getContentData()); Parser parser = new PDFParser(); ContentHandler contentHandler = new BodyContentHandler(); Metadata metadata = new Metadata(); ParseContext context = new ParseContext(); try { parser.parse(is, contentHandler, metadata, context); String title = Optional .ofNullable(metadata.get(TikaCoreProperties.TITLE)) .flatMap(s -> s.isEmpty() ? Optional.empty() : Optional.of(s)) .orElse(new File(page.getWebURL().getPath()).getName()); String descriptionOrContents = contentHandler.toString(); if (title != null && !title.isEmpty() && descriptionOrContents != null && !descriptionOrContents.isEmpty()) { JsonWriter .getInstance() .write(CrawlDocument .create(url, title, descriptionOrContents, descriptionOrContents, "pdf")); } else { logger.info("Invalid PDF title: {}, contents: {}, URL = {}", title, descriptionOrContents, url); } } catch (IOException | SAXException | TikaException e) { e.printStackTrace(); } } else { logger.info("Unknown Contents: URL = {}", url); } } }
クロールしたドキュメントがHTMLだった場合は、以下のような処理になります。
if (page.getParseData() instanceof HtmlParseData) { logger.info("Contents is HTML: URL = {}", url); HtmlParseData htmlParseData = (HtmlParseData) page.getParseData(); String html = htmlParseData.getHtml(); Set<WebURL> links = htmlParseData.getOutgoingUrls(); logger.info("Number of outgoing links: {}", links.size()); Document jsoupDoc = Jsoup.parse(html); String title = jsoupDoc.title(); String description = jsoupDoc.select("meta[name=\"description\"]").attr("content"); String contents = jsoupDoc.body().text(); if (title != null && !title.isEmpty() && contents != null && !contents.isEmpty()) { try { JsonWriter.getInstance().write(CrawlDocument.create(url, title, description, contents, "html")); } catch (IOException e) { throw new UncheckedIOException(e); } } else { logger.info("Invalid HTML title: {}, contents: {}, URL = {}", title, contents, url); }
とりあえず、タイトルとdescriptionとbodyをぶち抜く、と。
Document jsoupDoc = Jsoup.parse(html); String title = jsoupDoc.title(); String description = jsoupDoc.select("meta[name=\"description\"]").attr("content"); String contents = jsoupDoc.body().text();
ここではタイトルとbodyの中身があれば、ドキュメントとしてはOKとしています。
if (title != null && !title.isEmpty() && contents != null && !contents.isEmpty()) { try { JsonWriter.getInstance().write(CrawlDocument.create(url, title, description, contents, "html")); } catch (IOException e) { throw new UncheckedIOException(e); } } else { logger.info("Invalid HTML title: {}, contents: {}, URL = {}", title, contents, url); }
PDFの場合は、Apache Tikaでパースします。
} else if (page.getContentType().startsWith("application/pdf") || url.endsWith(".pdf")) { logger.info("Contents is PDF: URL = {}", url); InputStream is = new ByteArrayInputStream(page.getContentData()); Parser parser = new PDFParser(); ContentHandler contentHandler = new BodyContentHandler(); Metadata metadata = new Metadata(); ParseContext context = new ParseContext(); try { parser.parse(is, contentHandler, metadata, context); String title = Optional .ofNullable(metadata.get(TikaCoreProperties.TITLE)) .flatMap(s -> s.isEmpty() ? Optional.empty() : Optional.of(s)) .orElse(new File(page.getWebURL().getPath()).getName()); String descriptionOrContents = contentHandler.toString(); if (title != null && !title.isEmpty() && descriptionOrContents != null && !descriptionOrContents.isEmpty()) { JsonWriter .getInstance() .write(CrawlDocument .create(url, title, descriptionOrContents, descriptionOrContents, "pdf")); } else { logger.info("Invalid PDF title: {}, contents: {}, URL = {}", title, descriptionOrContents, url); } } catch (IOException | SAXException | TikaException e) { e.printStackTrace(); }
WebページからのレスポンスをInputStreamにしておいて
InputStream is = new ByteArrayInputStream(page.getContentData()); Parser parser = new PDFParser(); ContentHandler contentHandler = new BodyContentHandler(); Metadata metadata = new Metadata(); ParseContext context = new ParseContext();
タイトルはメタデータから取得、設定されていなければURLのファイル名から取るようにしました。descriptionはPDFにあったものでもないので、とりあえずコンテンツそのものと同じということで…。
String title = Optional .ofNullable(metadata.get(TikaCoreProperties.TITLE)) .flatMap(s -> s.isEmpty() ? Optional.empty() : Optional.of(s)) .orElse(new File(page.getWebURL().getPath()).getName()); String descriptionOrContents = contentHandler.toString();
HTMLと同じように、タイトルとコンテンツがあればOKとしています。
if (title != null && !title.isEmpty() && descriptionOrContents != null && !descriptionOrContents.isEmpty()) { JsonWriter .getInstance() .write(CrawlDocument .create(url, title, descriptionOrContents, descriptionOrContents, "pdf")); } else { logger.info("Invalid PDF title: {}, contents: {}, URL = {}", title, descriptionOrContents, url); }
HTMLでもPDFでもなければ、破棄と。
} else { logger.info("Unknown Contents: URL = {}", url); }
mainメソッドを持ったクラスは、こんな感じになりました。
src/main/java/org/littlewings/mycrawler/Launcher.java
package org.littlewings.mycrawler; import java.io.BufferedWriter; import java.io.File; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import edu.uci.ics.crawler4j.crawler.CrawlConfig; import edu.uci.ics.crawler4j.crawler.CrawlController; import edu.uci.ics.crawler4j.fetcher.PageFetcher; import edu.uci.ics.crawler4j.robotstxt.RobotstxtConfig; import edu.uci.ics.crawler4j.robotstxt.RobotstxtServer; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.impl.HttpSolrClient; import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest; import org.apache.solr.common.util.NamedList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Launcher { public static void main(String... args) throws Exception { Logger logger = LoggerFactory.getLogger(Launcher.class); // JSONファイルの作成 String indexJsonFile = "index-data.json"; BufferedWriter writer = Files.newBufferedWriter(Paths.get(indexJsonFile), StandardCharsets.UTF_8); JsonWriter jsonWriter = JsonWriter.getInstance(writer); jsonWriter.init(); // Crawlerの設定 String crawlStorageFolder = "data/crawl/strage"; int numberOfCrawlers = 7; // スレッド数 CrawlConfig config = new CrawlConfig(); config.setCrawlStorageFolder(crawlStorageFolder); config.setPolitenessDelay(2 * 1000); // 2秒ごとにリクエスト config.setIncludeBinaryContentInCrawling(true); // バイナリファイルも対象に config.setMaxDownloadSize(15 * 1024 * 1024); // 最大ダウンロードサイズを調整 // HttpClientの設定 PageFetcher pageFetcher = new PageFetcher(config); RobotstxtConfig robotstxtConfig = new RobotstxtConfig(); RobotstxtServer robotstxtServer = new RobotstxtServer(robotstxtConfig, pageFetcher); CrawlController controller = new CrawlController(config, pageFetcher, robotstxtServer); // クロール開始ポイント設定 logger.info("add Seed URL under... {}", "http://info.vmware.com/content/apac_jp_co_techresources"); controller.addSeed("http://info.vmware.com/content/apac_jp_co_techresources"); // クロール開始 logger.info("Start crawler."); controller.start(MyLittleCrawler.class, numberOfCrawlers); // クロール後、JSONファイル書き出し終了 jsonWriter.end(); jsonWriter.close(); writer.close(); logger.info("JSON file[{}] created.", indexJsonFile); // Solrへドキュメント更新 SolrClient solrClient = new HttpSolrClient("http://localhost:8983/solr/mycore"); ContentStreamUpdateRequest updateRequest = new ContentStreamUpdateRequest("/update"); updateRequest.addFile(new File(indexJsonFile), "application/json"); NamedList<Object> named = solrClient.request(updateRequest); logger.info("Solr request result = {}.", named); // コミットおよびオプティマイズ logger.info("commit = {}", solrClient.commit().getResponse()); logger.info("optimize = {}", solrClient.optimize().getResponse()); // 後始末 solrClient.close(); } }
最初に、JSONファイルを開いておきます。
// JSONファイルの作成 String indexJsonFile = "index-data.json"; BufferedWriter writer = Files.newBufferedWriter(Paths.get(indexJsonFile), StandardCharsets.UTF_8); JsonWriter jsonWriter = JsonWriter.getInstance(writer); jsonWriter.init();
このファイルは、クローラーのインスタンスによって随時書き込まれていきます。
crawler4jの設定時に、CrawlConfig#setIncludeBinaryContentInCrawlingでバイナリファイルもクローリング対象にしておかないと、PDFを取得してくれません。また、今回のクローリング対象はドキュメントサイズがcrawler4jのデフォルトの最大ダウンロードサイズよりも大きかったので、広げています。
// Crawlerの設定 String crawlStorageFolder = "data/crawl/strage"; int numberOfCrawlers = 7; // スレッド数 CrawlConfig config = new CrawlConfig(); config.setCrawlStorageFolder(crawlStorageFolder); config.setPolitenessDelay(2 * 1000); // 2秒ごとにリクエスト config.setIncludeBinaryContentInCrawling(true); // バイナリファイルも対象に config.setMaxDownloadSize(15 * 1024 * 1024); // 最大ダウンロードサイズを調整
クローリングを開始して、終了すればJSONファイルを閉じます。
// クロール開始 logger.info("Start crawler."); controller.start(MyLittleCrawler.class, numberOfCrawlers); // クロール後、JSONファイル書き出し終了 jsonWriter.end(); jsonWriter.close(); writer.close();
そして、最後に作成したJSONファイルを、UpdateHandlerで叩き込みます。
// Solrへドキュメント更新 SolrClient solrClient = new HttpSolrClient("http://localhost:8983/solr/mycore"); ContentStreamUpdateRequest updateRequest = new ContentStreamUpdateRequest("/update"); updateRequest.addFile(new File(indexJsonFile), "application/json"); NamedList<Object> named = solrClient.request(updateRequest); logger.info("Solr request result = {}.", named); // コミットおよびオプティマイズ logger.info("commit = {}", solrClient.commit().getResponse()); logger.info("optimize = {}", solrClient.optimize().getResponse()); // 後始末 solrClient.close();
これで、終了。
動かしてみる
これで動作させてみると、クローリングが始まりHTMLやPDFの取得、解析が始まります。
[main] INFO edu.uci.ics.crawler4j.crawler.CrawlController - Deleted contents of: data/crawl/strage/frontier ( as you have configured resumable crawling to false ) [main] INFO org.littlewings.mycrawler.Launcher - add Seed URL under... http://info.vmware.com/content/apac_jp_co_techresources [main] INFO org.littlewings.mycrawler.Launcher - Start crawler. [main] INFO edu.uci.ics.crawler4j.crawler.CrawlController - Crawler 1 started [main] INFO edu.uci.ics.crawler4j.crawler.CrawlController - Crawler 2 started [main] INFO edu.uci.ics.crawler4j.crawler.CrawlController - Crawler 3 started [main] INFO edu.uci.ics.crawler4j.crawler.CrawlController - Crawler 4 started [main] INFO edu.uci.ics.crawler4j.crawler.CrawlController - Crawler 5 started [main] INFO edu.uci.ics.crawler4j.crawler.CrawlController - Crawler 6 started [main] INFO edu.uci.ics.crawler4j.crawler.CrawlController - Crawler 7 started [Crawler 1] INFO org.littlewings.mycrawler.MyLittleCrawler - URL: http://info.vmware.com/content/apac_jp_co_techresources, Content-Type: text/html; charset=utf-8 [Crawler 1] INFO org.littlewings.mycrawler.MyLittleCrawler - Contents is HTML: URL = http://info.vmware.com/content/apac_jp_co_techresources [Crawler 1] INFO org.littlewings.mycrawler.MyLittleCrawler - Number of outgoing links: 292 [Crawler 3] INFO org.littlewings.mycrawler.MyLittleCrawler - URL: http://info.vmware.com/content/APAC_JP_VMware_Careers/?src=WWW_Careers_JP_CompanyMegaBanner_CareersSearchJob, Content-Type: text/html; charset=utf-8 [Crawler 3] INFO org.littlewings.mycrawler.MyLittleCrawler - Contents is HTML: URL = http://info.vmware.com/content/APAC_JP_VMware_Careers/?src=WWW_Careers_JP_CompanyMegaBanner_CareersSearchJob [Crawler 3] INFO org.littlewings.mycrawler.MyLittleCrawler - Number of outgoing links: 42
そして、クローリングが終了すると
[Thread-1] INFO edu.uci.ics.crawler4j.crawler.CrawlController - It looks like no thread is working, waiting for 10 seconds to make sure... [Thread-1] INFO edu.uci.ics.crawler4j.crawler.CrawlController - No thread is working and no more URLs are in queue waiting for another 10 seconds to make sure... [Thread-1] INFO edu.uci.ics.crawler4j.crawler.CrawlController - All of the crawlers are stopped. Finishing the process... [Thread-1] INFO edu.uci.ics.crawler4j.crawler.CrawlController - Waiting for 10 seconds before final clean up...
JSONファイルを閉じて、Solrのインデックスを更新します。
[main] INFO org.littlewings.mycrawler.Launcher - JSON file[index-data.json] created. [main] INFO org.littlewings.mycrawler.Launcher - Solr request result = {responseHeader={status=0,QTime=885}}. [main] INFO org.littlewings.mycrawler.Launcher - commit = {responseHeader={status=0,QTime=118}} [main] INFO org.littlewings.mycrawler.Launcher - optimize = {responseHeader={status=0,QTime=1}}
できあがった、JSONファイルの一部はこんな感じです。
$ head index-data.json [ { "url" : "http://info.vmware.com/content/apac_jp_co_techresources", "title" : "テクニカル リソース センター - VMware", "description" : "Technical Papers, Technical Resources", "contents" : "VMware テクニカル リソース センター クラウドコンピューティング Title Revised VMware とクラウド コンピューティング 14/01/2011 VMware 企業データシート:ITの革新を加速する“Your Cloud.” 〜省略〜
それでは、Solrに対して検索してみます。
$ curl 'http://localhost:8983/solr/mycore/select?wt=json&indent=true' -d '{ "query": "*:*" }' { "responseHeader":{ "status":0, "QTime":0, "params":{ "indent":"true", "json":"{ \"query\": \"*:*\" }", "wt":"json"}}, "response":{"numFound":40,"start":0,"docs":[ { "url":"http://info.vmware.com/content/apac_jp_co_techresources", "title":"テクニカル リソース センター - VMware", "description":"Technical Papers, Technical Resources", "contents":"VMware テクニカル リソース センター クラウドコンピューティング Title Revised VMware とクラウド コンピューティング 14/01/2011 VMware 企業データシート:ITの革新を加速する“ 〜省略〜
全部で40ドキュメントあるみたいです。
範囲をHTMLに絞ってみます。
$ curl 'http://localhost:8983/solr/mycore/select?wt=json&indent=true' -d '{ "query": "file_type:html" }' { "responseHeader":{ "status":0, "QTime":0, "params":{ "indent":"true", "json":"{ \"query\": \"file_type:html\" }", "wt":"json"}}, "response":{"numFound":29,"start":0,"docs":[ { "url":"http://info.vmware.com/content/apac_jp_co_techresources", "title":"テクニカル リソース センター - VMware", Your Cloud.” 22/01/2013 VMware のクラウド運用支援サービス 30/10/2012 \"Your Cloud.\" の実現に向けて 事後対応型の組織から革新型の組織への変革 30/10/2012 オンデマンドサービス 30/10/2012 VMwar 〜省略〜
29件ですね。
今度は、PDFの方で見てみます。
$ curl 'http://localhost:8983/solr/mycore/select?wt=json&indent=true' -d '{ "query": "file_type:pdf" }' { "responseHeader":{ "status":0, "QTime":1, "params":{ "indent":"true", "json":"{ \"query\": \"file_type:pdf\" }", "wt":"json"}}, "response":{"numFound":11,"start":0,"docs":[ { "url":"http://www.vmware.com/files/jp/pdf/products/vsphere/VMware-vSphere-Evaluation-Guide-2-Advanced-Storage.pdf", "title":"VMware-vSphere-Evaluation-Guide-2-Advanced-Storage.pdf", "description":"\nVMware vSphere® 5.0 \n評価ガイド\nVol.2: 高度なストレージ機能\n\nテクニカル ホワイト ペーパー\n\n\n\nVMware vSphere 5.0 評価ガイド \nVol.2\n\nテクニカル ホワイ 〜省略〜
こちらは、残りの11件ですね。
というわけで、なんとなくそれっぽいのができあがりました。